Tweaks to overrides (added draggable bounds, take timezone into consideration) (#4553)
# What this PR does Part of https://github.com/grafana/oncall/issues/4428 ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/4547
This commit is contained in:
parent
615081a112
commit
16e98da64a
21 changed files with 326 additions and 163 deletions
|
|
@ -3,7 +3,7 @@ import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup';
|
|||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
||||
import { createOnCallScheduleWithRotation } from '../utils/schedule';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
test('we can create an oncall schedule + receive an alert', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
|
@ -11,7 +11,7 @@ test('we can create an oncall schedule + receive an alert', async ({ adminRolePa
|
|||
const integrationName = generateRandomValue();
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
await createEscalationChain(
|
||||
page,
|
||||
escalationChainName,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { createEscalationChain, EscalationStep } from '../utils/escalationChain'
|
|||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
||||
import { goToGrafanaPage, goToOnCallPage } from '../utils/navigation';
|
||||
import { createOnCallScheduleWithRotation } from '../utils/schedule';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
/**
|
||||
* Insights is dependent on Scenes which were only added in Grafana 10.0.0
|
||||
|
|
@ -66,7 +66,7 @@ test.describe.skip('Insights', () => {
|
|||
const escalationChainName = generateRandomValue();
|
||||
const integrationName = generateRandomValue();
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
await createEscalationChain(
|
||||
page,
|
||||
escalationChainName,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
import { test, expect } from '../fixtures';
|
||||
import { test, expect, Locator } from '../fixtures';
|
||||
import { MOSCOW_TIMEZONE } from '../utils/constants';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallScheduleWithRotation, getOverrideFormDateInputs } from '../utils/schedule';
|
||||
import { setTimezoneInProfile } from '../utils/grafanaProfile';
|
||||
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
|
||||
|
||||
test('default dates in override creation modal are correct', async ({ adminRolePage }) => {
|
||||
test('Default dates in override creation modal are set to today', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
await clickButton({ page, buttonText: 'Add override' });
|
||||
|
||||
|
|
@ -20,3 +22,39 @@ test('default dates in override creation modal are correct', async ({ adminRoleP
|
|||
expect(overrideFormDateInputs.start.isSame(expectedStart)).toBe(true);
|
||||
expect(overrideFormDateInputs.end.isSame(expectedEnd)).toBe(true);
|
||||
});
|
||||
|
||||
test('Fills in override time and reacts to timezone change', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
||||
await setTimezoneInProfile(page, MOSCOW_TIMEZONE); // UTC+3
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName, false);
|
||||
|
||||
await clickButton({ page, buttonText: 'Add override' });
|
||||
|
||||
const overrideStartEl = page.getByTestId('override-start');
|
||||
await changeDatePickerTime(overrideStartEl, '02');
|
||||
await expect(overrideStartEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('02:00');
|
||||
|
||||
const overrideEndEl = page.getByTestId('override-end');
|
||||
await changeDatePickerTime(overrideEndEl, '12');
|
||||
await expect(overrideEndEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('12:00');
|
||||
|
||||
await page.getByRole('dialog').click(); // clear focus
|
||||
|
||||
await page.getByTestId('timezone-select').locator('svg').click();
|
||||
await page.getByText('GMT', { exact: true }).click();
|
||||
|
||||
// expect times to go back by -3
|
||||
await expect(overrideStartEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('23:00');
|
||||
await expect(overrideEndEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('09:00');
|
||||
|
||||
async function changeDatePickerTime(element: Locator, value: string) {
|
||||
await element.getByRole('img').click();
|
||||
// set minutes to {value}
|
||||
await page.locator('.rc-time-picker-panel').getByRole('button', { name: value }).first().click();
|
||||
// set seconds to 00
|
||||
await page.getByRole('button', { name: '00' }).nth(1).click();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
43
grafana-plugin/e2e-tests/schedules/addRotation.test.ts
Normal file
43
grafana-plugin/e2e-tests/schedules/addRotation.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { test, expect, Locator } from '../fixtures';
|
||||
import { MOSCOW_TIMEZONE } from '../utils/constants';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { setTimezoneInProfile } from '../utils/grafanaProfile';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
test('Fills in Rotation time and reacts to timezone change', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
||||
await setTimezoneInProfile(page, MOSCOW_TIMEZONE); // UTC+3
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName, false);
|
||||
|
||||
await clickButton({ page, buttonText: 'Add rotation' });
|
||||
// enable Rotation End
|
||||
await page.getByTestId('rotation-end').getByLabel('Toggle switch').click();
|
||||
|
||||
const startEl = page.getByTestId('rotation-start');
|
||||
await changeDatePickerTime(startEl, '02');
|
||||
await expect(startEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('02:00');
|
||||
|
||||
const endEl = page.getByTestId('rotation-end');
|
||||
await changeDatePickerTime(endEl, '12');
|
||||
await expect(endEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('12:00');
|
||||
|
||||
await page.getByRole('dialog').click(); // clear focus
|
||||
|
||||
await page.getByTestId('timezone-select').locator('svg').click();
|
||||
await page.getByText('GMT', { exact: true }).click();
|
||||
|
||||
// expect times to go back by -3
|
||||
await expect(startEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('23:00');
|
||||
await expect(endEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('09:00');
|
||||
|
||||
async function changeDatePickerTime(element: Locator, value: string) {
|
||||
await element.getByRole('img').click();
|
||||
// set minutes to {value}
|
||||
await page.locator('.rc-time-picker-panel').getByRole('button', { name: value }).first().click();
|
||||
// set seconds to 00
|
||||
await page.getByRole('button', { name: '00' }).nth(1).click();
|
||||
}
|
||||
});
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { test, expect } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallScheduleWithRotation } from '../utils/schedule';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
test('check schedule quality for simple 1-user schedule', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
const scheduleQualityElement = page.getByTestId('schedule-quality');
|
||||
await scheduleQualityElement.waitFor({ state: 'visible' });
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { test, expect } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallScheduleWithRotation, createRotation } from '../utils/schedule';
|
||||
import { createOnCallSchedule, createRotation } from '../utils/schedule';
|
||||
|
||||
test(`user can see the other user's details`, async ({ adminRolePage, editorRolePage }) => {
|
||||
const { page, userName: adminUserName } = adminRolePage;
|
||||
const editorUserName = editorRolePage.userName;
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, adminUserName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, adminUserName);
|
||||
await createRotation(page, editorUserName, false);
|
||||
|
||||
await page.waitForTimeout(1_000);
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { HTML_ID } from 'utils/DOM';
|
|||
|
||||
import { expect, test } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallScheduleWithRotation } from '../utils/schedule';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
test.skip('schedule view (week/2 weeks/month) toggler works', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
// ScheduleView.OneWeek is selected by default
|
||||
expect(await page.getByLabel(ScheduleView.OneWeek, { exact: true }).isChecked()).toBe(true);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { expect, test } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
import { createOnCallScheduleWithRotation } from '../utils/schedule';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
test('schedule calendar and list of schedules is correctly displayed', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
await goToOnCallPage(page, 'schedules');
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@ import isoWeek from 'dayjs/plugin/isoWeek';
|
|||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
import { test } from '../fixtures';
|
||||
import { MOSCOW_TIMEZONE } from '../utils/constants';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { setTimezoneInProfile } from '../utils/grafanaProfile';
|
||||
import { createOnCallScheduleWithRotation } from '../utils/schedule';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(isoWeek);
|
||||
|
||||
const MOSCOW_TIMEZONE = 'Europe/Moscow';
|
||||
|
||||
test.use({ timezoneId: MOSCOW_TIMEZONE }); // GMT+3 the whole year
|
||||
const currentUtcTimeHour = dayjs().utc().format('HH');
|
||||
const currentUtcDate = dayjs().utc().format('DD MMM');
|
||||
|
|
@ -25,7 +24,7 @@ test('dates in schedule are correct according to selected current timezone', asy
|
|||
await setTimezoneInProfile(page, MOSCOW_TIMEZONE);
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
// Current timezone is selected by default to currently logged in user timezone
|
||||
await expect(page.getByTestId('timezone-select')).toHaveText('GMT+3');
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@ export const GRAFANA_ADMIN_PASSWORD = process.env.GRAFANA_ADMIN_PASSWORD || 'onc
|
|||
|
||||
export const IS_OPEN_SOURCE = (process.env.IS_OPEN_SOURCE || 'true').toLowerCase() === 'true';
|
||||
export const IS_CLOUD = !IS_OPEN_SOURCE;
|
||||
|
||||
export const MOSCOW_TIMEZONE = 'Europe/Moscow';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { Page } from '@playwright/test';
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import { goToGrafanaPage } from './navigation';
|
||||
|
||||
export const setTimezoneInProfile = async (page: Page, timezone: string) => {
|
||||
await goToGrafanaPage(page, '/profile');
|
||||
await expect(page.getByLabel('Time zone picker')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Time zone picker').click();
|
||||
await page.getByLabel('Select options menu').getByText(timezone).click();
|
||||
await page.getByTestId('data-testid-shared-prefs-save').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // wait for reload
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import dayjs from 'dayjs';
|
|||
import { clickButton, selectDropdownValue } from './forms';
|
||||
import { goToOnCallPage } from './navigation';
|
||||
|
||||
export const createOnCallScheduleWithRotation = async (
|
||||
export const createOnCallSchedule = async (
|
||||
page: Page,
|
||||
scheduleName: string,
|
||||
userName: string
|
||||
userName: string,
|
||||
withRotation = true
|
||||
): Promise<void> => {
|
||||
// go to the schedules page
|
||||
await goToOnCallPage(page, 'schedules');
|
||||
|
|
@ -22,7 +23,9 @@ export const createOnCallScheduleWithRotation = async (
|
|||
// Add a new layer w/ the current user to it
|
||||
await clickButton({ page, buttonText: 'Create Schedule' });
|
||||
|
||||
await createRotation(page, userName);
|
||||
if (withRotation) {
|
||||
await createRotation(page, userName);
|
||||
}
|
||||
};
|
||||
|
||||
export const createRotation = async (page: Page, userName: string, isFirstScheduleRotation = true) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { Dayjs, ManipulateType } from 'dayjs';
|
||||
import { DraggableData } from 'react-draggable';
|
||||
|
||||
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { GRAFANA_HEADER_HEIGHT, GRAFANA_LEGACY_SIDEBAR_WIDTH } from 'utils/consts';
|
||||
|
||||
import { RepeatEveryPeriod } from './RotationForm.types';
|
||||
|
||||
|
|
@ -173,3 +177,40 @@ export const dayJSAddWithDSTFixed = ({
|
|||
|
||||
return newDateCandidate.add(diff, 'minutes');
|
||||
};
|
||||
|
||||
export function getDraggableModalCoordinatesOnInit(
|
||||
data: DraggableData,
|
||||
offsetTop: number
|
||||
): {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
} {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scrollBarReferenceElements = document.querySelectorAll<HTMLElement>('.scrollbar-view');
|
||||
// top navbar display has 2 scrollbar-view elements (navbar & content)
|
||||
const baseReferenceElRect = (
|
||||
scrollBarReferenceElements.length === 1 ? scrollBarReferenceElements[0] : scrollBarReferenceElements[1]
|
||||
).getBoundingClientRect();
|
||||
|
||||
const { right, bottom } = baseReferenceElRect;
|
||||
|
||||
return isTopNavbar()
|
||||
? {
|
||||
// values are adjusted by any padding/margin differences
|
||||
left: -data.node.offsetLeft + 4,
|
||||
right: right - (data.node.offsetLeft + data.node.offsetWidth) - 12,
|
||||
top: -offsetTop + GRAFANA_HEADER_HEIGHT + 4,
|
||||
bottom: bottom - data.node.offsetHeight - offsetTop - 12,
|
||||
}
|
||||
: {
|
||||
left: -data.node.offsetLeft + 4 + GRAFANA_LEGACY_SIDEBAR_WIDTH,
|
||||
right: right - (data.node.offsetLeft + data.node.offsetWidth) - 12,
|
||||
top: -offsetTop + 4,
|
||||
bottom: bottom - data.node.offsetHeight - offsetTop - 12,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
TimeUnit,
|
||||
timeUnitsToSeconds,
|
||||
TIME_UNITS_ORDER,
|
||||
getDraggableModalCoordinatesOnInit,
|
||||
} from 'containers/RotationForm/RotationForm.helpers';
|
||||
import { RepeatEveryPeriod } from 'containers/RotationForm/RotationForm.types';
|
||||
import { DateTimePicker } from 'containers/RotationForm/parts/DateTimePicker';
|
||||
|
|
@ -47,6 +48,7 @@ import { DaysSelector } from 'containers/RotationForm/parts/DaysSelector';
|
|||
import { DeletionModal } from 'containers/RotationForm/parts/DeletionModal';
|
||||
import { TimeUnitSelector } from 'containers/RotationForm/parts/TimeUnitSelector';
|
||||
import { UserItem } from 'containers/RotationForm/parts/UserItem';
|
||||
import { calculateScheduleFormOffset } from 'containers/Rotations/Rotations.helpers';
|
||||
import { getShiftName } from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift } from 'models/schedule/schedule.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
|
@ -58,12 +60,11 @@ import {
|
|||
getUTCWeekStart,
|
||||
getWeekStartString,
|
||||
toDateWithTimezoneOffset,
|
||||
toDateWithTimezoneOffsetAtMidnight,
|
||||
} from 'pages/schedule/Schedule.helpers';
|
||||
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { getCoords, waitForElement } from 'utils/DOM';
|
||||
import { GRAFANA_HEADER_HEIGHT, GRAFANA_LEGACY_SIDEBAR_WIDTH } from 'utils/consts';
|
||||
import { useDebouncedCallback } from 'utils/hooks';
|
||||
import { GRAFANA_HEADER_HEIGHT } from 'utils/consts';
|
||||
import { useDebouncedCallback, useResize } from 'utils/hooks';
|
||||
|
||||
import styles from './RotationForm.module.css';
|
||||
|
||||
|
|
@ -85,17 +86,11 @@ interface RotationFormProps {
|
|||
|
||||
const getStartShift = (start: dayjs.Dayjs, timezoneOffset: number, isNewRotation = false) => {
|
||||
if (isNewRotation) {
|
||||
// all new rotations default to midnight in selected timezone offset
|
||||
return toDateWithTimezoneOffset(start, timezoneOffset)
|
||||
.set('date', 1)
|
||||
.set('year', start.year())
|
||||
.set('month', start.month())
|
||||
.set('date', start.date())
|
||||
.set('hour', 0)
|
||||
.set('minute', 0)
|
||||
.set('second', 0);
|
||||
// default to midnight for new rotations
|
||||
return toDateWithTimezoneOffsetAtMidnight(start, timezoneOffset);
|
||||
}
|
||||
|
||||
// not always midnight
|
||||
return toDateWithTimezoneOffset(start, timezoneOffset);
|
||||
};
|
||||
|
||||
|
|
@ -156,12 +151,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
const [showDeleteRotationConfirmation, setShowDeleteRotationConfirmation] = useState<boolean>(false);
|
||||
const debouncedOnResize = useDebouncedCallback(onResize, 250);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', debouncedOnResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', debouncedOnResize);
|
||||
};
|
||||
}, []);
|
||||
useResize(debouncedOnResize);
|
||||
|
||||
useEffect(() => {
|
||||
if (rotationStart.isBefore(shiftStart)) {
|
||||
|
|
@ -178,7 +168,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
useEffect(() => {
|
||||
(async () => {
|
||||
if (isOpen) {
|
||||
setOffsetTop(await calculateOffsetTop());
|
||||
setOffsetTop(await calculateScheduleFormOffset(`.${cx('draggable')}`));
|
||||
}
|
||||
})();
|
||||
}, [isOpen]);
|
||||
|
|
@ -612,6 +602,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
Starts
|
||||
</Text>
|
||||
}
|
||||
data-testid="rotation-start"
|
||||
>
|
||||
<DateTimePicker
|
||||
value={rotationStart}
|
||||
|
|
@ -636,6 +627,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
/>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
data-testid="rotation-end"
|
||||
>
|
||||
{endLess ? (
|
||||
<div style={{ lineHeight: '32px' }}>
|
||||
|
|
@ -771,7 +763,9 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
</div>
|
||||
<div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text type="secondary">Current timezone: {store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
<Text type="secondary">
|
||||
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
</Text>
|
||||
<HorizontalGroup>
|
||||
{shiftId !== 'new' && (
|
||||
<Tooltip content="Stop the current rotation and start a new one">
|
||||
|
|
@ -803,20 +797,9 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
);
|
||||
|
||||
async function onResize() {
|
||||
setDraggablePosition({ x: 0, y: await calculateOffsetTop() });
|
||||
}
|
||||
setOffsetTop(await calculateScheduleFormOffset(`.${cx('draggable')}`));
|
||||
|
||||
async function calculateOffsetTop(): Promise<number> {
|
||||
const elm = await waitForElement(`#layer${shiftId === 'new' ? layerPriority : shift?.priority_level}`);
|
||||
const modal = document.querySelector(`.${cx('draggable')}`) as HTMLDivElement;
|
||||
const coords = getCoords(elm);
|
||||
|
||||
const offsetTop = Math.max(
|
||||
Math.min(coords.top - modal?.offsetHeight - 10, document.body.offsetHeight - modal?.offsetHeight - 10),
|
||||
GRAFANA_HEADER_HEIGHT + 10
|
||||
);
|
||||
|
||||
return offsetTop;
|
||||
setDraggablePosition({ x: 0, y: 0 });
|
||||
}
|
||||
|
||||
function onDraggableInit(_e: DraggableEvent, data: DraggableData) {
|
||||
|
|
@ -824,30 +807,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const scrollBarReferenceElements = document.querySelectorAll<HTMLElement>('.scrollbar-view');
|
||||
// top navbar display has 2 scrollbar-view elements (navbar & content)
|
||||
const baseReferenceElRect = (
|
||||
scrollBarReferenceElements.length === 1 ? scrollBarReferenceElements[0] : scrollBarReferenceElements[1]
|
||||
).getBoundingClientRect();
|
||||
|
||||
const { right, bottom } = baseReferenceElRect;
|
||||
|
||||
setDraggableBounds(
|
||||
isTopNavbar()
|
||||
? {
|
||||
// values are adjusted by any padding/margin differences
|
||||
left: -data.node.offsetLeft + 4,
|
||||
right: right - (data.node.offsetLeft + data.node.offsetWidth) - 12,
|
||||
top: -offsetTop + GRAFANA_HEADER_HEIGHT + 4,
|
||||
bottom: bottom - data.node.offsetHeight - offsetTop - 12,
|
||||
}
|
||||
: {
|
||||
left: -data.node.offsetLeft + 4 + GRAFANA_LEGACY_SIDEBAR_WIDTH,
|
||||
right: right - (data.node.offsetLeft + data.node.offsetWidth) - 12,
|
||||
top: -offsetTop + 4,
|
||||
bottom: bottom - data.node.offsetHeight - offsetTop - 12,
|
||||
}
|
||||
);
|
||||
setDraggableBounds(getDraggableModalCoordinatesOnInit(data, offsetTop));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,22 +3,22 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import { IconButton, VerticalGroup, HorizontalGroup, Field, Button, useTheme2 } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import Draggable from 'react-draggable';
|
||||
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
|
||||
|
||||
import { Modal } from 'components/Modal/Modal';
|
||||
import { Tag } from 'components/Tag/Tag';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { UserGroups } from 'components/UserGroups/UserGroups';
|
||||
import { WithConfirm } from 'components/WithConfirm/WithConfirm';
|
||||
import { calculateScheduleFormOffset } from 'containers/Rotations/Rotations.helpers';
|
||||
import { getShiftName } from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift } from 'models/schedule/schedule.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { getDateTime, getUTCString, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { HTML_ID, getCoords, waitForElement } from 'utils/DOM';
|
||||
import { GRAFANA_HEADER_HEIGHT } from 'utils/consts';
|
||||
import { useDebouncedCallback } from 'utils/hooks';
|
||||
import { useDebouncedCallback, useResize } from 'utils/hooks';
|
||||
|
||||
import { getDraggableModalCoordinatesOnInit } from './RotationForm.helpers';
|
||||
import { DateTimePicker } from './parts/DateTimePicker';
|
||||
import { UserItem } from './parts/UserItem';
|
||||
|
||||
|
|
@ -39,6 +39,9 @@ interface RotationFormProps {
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
||||
const store = useStore();
|
||||
const theme = useTheme2();
|
||||
|
||||
const {
|
||||
onHide,
|
||||
onCreate,
|
||||
|
|
@ -46,16 +49,18 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
onUpdate,
|
||||
onDelete,
|
||||
shiftId,
|
||||
shiftStart: propsShiftStart = dayjs().startOf('day').add(1, 'day'),
|
||||
shiftStart: propsShiftStart = store.timezoneStore.calendarStartDate,
|
||||
shiftEnd: propsShiftEnd,
|
||||
shiftColor: shiftColorProp,
|
||||
} = props;
|
||||
|
||||
const store = useStore();
|
||||
const theme = useTheme2();
|
||||
|
||||
const [rotationName, setRotationName] = useState<string>(shiftId === 'new' ? 'Override' : 'Update override');
|
||||
|
||||
const [draggablePosition, setDraggablePosition] = useState<{ x: number; y: number }>(undefined);
|
||||
const [bounds, setDraggableBounds] = useState<{ left: number; right: number; top: number; bottom: number }>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const [shiftStart, setShiftStart] = useState<dayjs.Dayjs>(propsShiftStart);
|
||||
const [shiftEnd, setShiftEnd] = useState<dayjs.Dayjs>(propsShiftEnd || propsShiftStart.add(24, 'hours'));
|
||||
|
||||
|
|
@ -66,6 +71,10 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
const [errors, setErrors] = useState<{ [key: string]: string[] }>({});
|
||||
const shiftColor = shiftColorProp || theme.colors.warning.main;
|
||||
|
||||
const debouncedOnResize = useDebouncedCallback(onResize, 250);
|
||||
|
||||
useResize(debouncedOnResize);
|
||||
|
||||
const updateShiftStart = useCallback(
|
||||
(value) => {
|
||||
const diff = shiftEnd.diff(shiftStart);
|
||||
|
|
@ -79,15 +88,7 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
useEffect(() => {
|
||||
(async () => {
|
||||
if (isOpen) {
|
||||
const elm = await waitForElement(`#${HTML_ID.SCHEDULE_OVERRIDES_AND_SWAPS}`);
|
||||
const modal = document.querySelector(`.${cx('draggable')}`) as HTMLDivElement;
|
||||
const coords = getCoords(elm);
|
||||
const offsetTop = Math.min(
|
||||
Math.max(coords.top - modal?.offsetHeight - 10, GRAFANA_HEADER_HEIGHT + 10),
|
||||
document.body.offsetHeight - modal?.offsetHeight - 10
|
||||
);
|
||||
|
||||
setOffsetTop(offsetTop);
|
||||
setOffsetTop(await calculateScheduleFormOffset(`.${cx('draggable')}`));
|
||||
}
|
||||
})();
|
||||
}, [isOpen]);
|
||||
|
|
@ -102,6 +103,11 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
}
|
||||
}, [shiftId]);
|
||||
|
||||
useEffect(() => {
|
||||
setShiftStart(toDateWithTimezoneOffset(shiftStart, store.timezoneStore.selectedTimezoneOffset));
|
||||
setShiftEnd(toDateWithTimezoneOffset(shiftEnd, store.timezoneStore.selectedTimezoneOffset));
|
||||
}, [store.timezoneStore.selectedTimezoneOffset]);
|
||||
|
||||
const params = useMemo(
|
||||
() => ({
|
||||
rotation_start: getUTCString(shiftStart),
|
||||
|
|
@ -200,7 +206,15 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
width="430px"
|
||||
onDismiss={onHide}
|
||||
contentElement={(props, children) => (
|
||||
<Draggable handle=".drag-handler" defaultClassName={cx('draggable')} positionOffset={{ x: 0, y: offsetTop }}>
|
||||
<Draggable
|
||||
handle=".drag-handler"
|
||||
defaultClassName={cx('draggable')}
|
||||
positionOffset={{ x: 0, y: offsetTop }}
|
||||
position={draggablePosition}
|
||||
bounds={{ ...bounds } || 'body'}
|
||||
onStart={onDraggableInit}
|
||||
onStop={(_e, data) => setDraggablePosition({ x: data.x, y: data.y })}
|
||||
>
|
||||
<div {...props}>{children}</div>
|
||||
</Draggable>
|
||||
)}
|
||||
|
|
@ -215,7 +229,7 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
{shiftId !== 'new' && (
|
||||
<WithConfirm>
|
||||
<WithConfirm title="Are you sure you want to delete override?">
|
||||
<IconButton variant="secondary" tooltip="Delete" name="trash-alt" onClick={handleDeleteClick} />
|
||||
</WithConfirm>
|
||||
)}
|
||||
|
|
@ -228,49 +242,70 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<div className={cx('override-form-content')} data-testid="override-inputs">
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<Field
|
||||
className={cx('date-time-picker')}
|
||||
label={
|
||||
<Text type="primary" size="small">
|
||||
Override period start
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<DateTimePicker
|
||||
disabled={disabled}
|
||||
value={shiftStart}
|
||||
onChange={updateShiftStart}
|
||||
error={errors.shift_start}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
className={cx('date-time-picker')}
|
||||
label={
|
||||
<Text type="primary" size="small">
|
||||
Override period end
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<DateTimePicker disabled={disabled} value={shiftEnd} onChange={setShiftEnd} error={errors.shift_end} />
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
<UserGroups
|
||||
disabled={disabled}
|
||||
value={userGroups}
|
||||
onChange={setUserGroups}
|
||||
isMultipleGroups={false}
|
||||
renderUser={(pk: ApiSchemas['User']['pk']) => (
|
||||
<UserItem pk={pk} shiftColor={shiftColor} shiftStart={params.shift_start} shiftEnd={params.shift_end} />
|
||||
)}
|
||||
showError={Boolean(errors.rolling_users)}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
|
||||
<div className={cx('container')}>
|
||||
<div className={cx('override-form-content')} data-testid="override-inputs">
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<Field
|
||||
className={cx('date-time-picker')}
|
||||
data-testid="override-start"
|
||||
label={
|
||||
<Text type="primary" size="small">
|
||||
Override period start
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<DateTimePicker
|
||||
disabled={disabled}
|
||||
value={shiftStart}
|
||||
utcOffset={store.timezoneStore.selectedTimezoneOffset}
|
||||
onChange={updateShiftStart}
|
||||
error={errors.shift_start}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
className={cx('date-time-picker')}
|
||||
data-testid="override-end"
|
||||
label={
|
||||
<Text type="primary" size="small">
|
||||
Override period end
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<DateTimePicker
|
||||
disabled={disabled}
|
||||
value={shiftEnd}
|
||||
utcOffset={store.timezoneStore.selectedTimezoneOffset}
|
||||
onChange={setShiftEnd}
|
||||
error={errors.shift_end}
|
||||
/>
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
|
||||
<UserGroups
|
||||
disabled={disabled}
|
||||
value={userGroups}
|
||||
onChange={setUserGroups}
|
||||
isMultipleGroups={false}
|
||||
renderUser={(pk: ApiSchemas['User']['pk']) => (
|
||||
<UserItem
|
||||
pk={pk}
|
||||
shiftColor={shiftColor}
|
||||
shiftStart={params.shift_start}
|
||||
shiftEnd={params.shift_end}
|
||||
/>
|
||||
)}
|
||||
showError={Boolean(errors.rolling_users)}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text type="secondary">Current timezone: {store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
<Text type="secondary">
|
||||
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
</Text>
|
||||
<HorizontalGroup>
|
||||
<Button variant="primary" onClick={handleCreate} disabled={disabled || !isFormValid}>
|
||||
{shiftId === 'new' ? 'Create' : 'Update'}
|
||||
|
|
@ -280,4 +315,18 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
</VerticalGroup>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
async function onResize() {
|
||||
setOffsetTop(await calculateScheduleFormOffset(`.${cx('draggable')}`));
|
||||
|
||||
setDraggablePosition({ x: 0, y: 0 });
|
||||
}
|
||||
|
||||
function onDraggableInit(_e: DraggableEvent, data: DraggableData) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDraggableBounds(getDraggableModalCoordinatesOnInit(data, offsetTop));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@ import { getColor, getOverrideColor } from 'models/schedule/schedule.helpers';
|
|||
import { Layer, Shift } from 'models/schedule/schedule.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers';
|
||||
import { waitForElement } from 'utils/DOM';
|
||||
|
||||
export const calculateScheduleFormOffset = async (queryClassName: string) => {
|
||||
const modal = await waitForElement(queryClassName);
|
||||
const modalHeight = modal.clientHeight;
|
||||
|
||||
return document.documentElement.scrollHeight / 2 - modalHeight / 2;
|
||||
};
|
||||
|
||||
// DatePickers will convert the date passed to local timezone, instead we want to use the date in the given timezone
|
||||
export const toDatePickerDate = (value: dayjs.Dayjs, timezoneOffset: number) => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { TimelineMarks } from 'containers/TimelineMarks/TimelineMarks';
|
|||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { getColor, getLayersFromStore, scheduleViewToDaysInOneRow } from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, ScheduleType, Shift, ShiftSwap, Event, Layer } from 'models/schedule/schedule.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { getCurrentTimeX, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -34,13 +33,11 @@ interface RotationsProps extends WithStoreProps {
|
|||
layerPriorityToShowRotationForm?: Layer['priority'];
|
||||
scheduleId: Schedule['id'];
|
||||
onShowRotationForm: (shiftId: Shift['id'] | 'new', layerPriority?: Layer['priority']) => void;
|
||||
onClick: (id: Shift['id'] | 'new') => void;
|
||||
onShowOverrideForm: (shiftId: 'new', shiftStart: dayjs.Dayjs, shiftEnd: dayjs.Dayjs) => void;
|
||||
onShowShiftSwapForm: (id: ShiftSwap['id'] | 'new', params?: Partial<ShiftSwap>) => void;
|
||||
onCreate: () => void;
|
||||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
onShiftSwapRequest: (beneficiary: ApiSchemas['User']['pk'], swap_start: string, swap_end: string) => void;
|
||||
disabled: boolean;
|
||||
filters: ScheduleFiltersType;
|
||||
onSlotClick?: (event: Event) => void;
|
||||
|
|
@ -362,4 +359,6 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
};
|
||||
}
|
||||
|
||||
export const Rotations = withMobXProviderContext(withTheme2(_Rotations));
|
||||
export const Rotations = withMobXProviderContext(withTheme2(_Rotations)) as unknown as React.ComponentClass<
|
||||
Omit<RotationsProps, 'store' | 'theme'>
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
SHIFT_SWAP_COLOR,
|
||||
} from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift, ShiftEvents, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { getCurrentTimeX } from 'pages/schedule/Schedule.helpers';
|
||||
import { getCurrentTimeX, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { HTML_ID } from 'utils/DOM';
|
||||
|
|
@ -39,7 +39,7 @@ interface ScheduleOverridesProps extends WithStoreProps {
|
|||
shiftEndToShowOverrideForm: dayjs.Dayjs;
|
||||
scheduleId: Schedule['id'];
|
||||
shiftIdToShowRotationForm?: Shift['id'] | 'new';
|
||||
onShowRotationForm: (shiftId: Shift['id'] | 'new') => void;
|
||||
onShowOverridesForm: (shiftId: Shift['id'] | 'new') => void;
|
||||
onShowShiftSwapForm: (id: ShiftSwap['id'] | 'new') => void;
|
||||
onCreate: () => void;
|
||||
onUpdate: () => void;
|
||||
|
|
@ -205,8 +205,14 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
shiftId={shiftIdToShowRotationForm}
|
||||
shiftColor={findColor(shiftIdToShowRotationForm, undefined, shifts)}
|
||||
scheduleId={scheduleId}
|
||||
shiftStart={propsShiftStartToShowOverrideForm || shiftStartToShowOverrideForm}
|
||||
shiftEnd={propsShiftEndToShowOverrideForm || shiftEndToShowOverrideForm}
|
||||
shiftStart={toDateWithTimezoneOffset(
|
||||
propsShiftStartToShowOverrideForm || shiftStartToShowOverrideForm,
|
||||
store.timezoneStore.selectedTimezoneOffset
|
||||
)}
|
||||
shiftEnd={toDateWithTimezoneOffset(
|
||||
propsShiftEndToShowOverrideForm || shiftEndToShowOverrideForm,
|
||||
store.timezoneStore.selectedTimezoneOffset
|
||||
)}
|
||||
onHide={() => {
|
||||
this.handleHide();
|
||||
|
||||
|
|
@ -241,7 +247,7 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
}
|
||||
|
||||
this.setState({ shiftStartToShowOverrideForm: shiftStart, shiftEndToShowOverrideForm: shiftEnd }, () => {
|
||||
this.onShowRotationForm(shiftId);
|
||||
this.onShowOverridesForm(shiftId);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -256,22 +262,24 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
this.setState(
|
||||
{ shiftStartToShowOverrideForm: store.timezoneStore.currentDateInSelectedTimezone.startOf('day') },
|
||||
() => {
|
||||
this.onShowRotationForm('new');
|
||||
this.onShowOverridesForm('new');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleHide = () => {
|
||||
this.setState({ shiftStartToShowOverrideForm: undefined, shiftEndToShowOverrideForm: undefined }, () => {
|
||||
this.onShowRotationForm(undefined);
|
||||
this.onShowOverridesForm(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
onShowRotationForm = (shiftId: Shift['id']) => {
|
||||
const { onShowRotationForm } = this.props;
|
||||
onShowOverridesForm = (shiftId: Shift['id']) => {
|
||||
const { onShowOverridesForm } = this.props;
|
||||
|
||||
onShowRotationForm(shiftId);
|
||||
onShowOverridesForm(shiftId);
|
||||
};
|
||||
}
|
||||
|
||||
export const ScheduleOverrides = withMobXProviderContext(withTheme2(_ScheduleOverrides));
|
||||
export const ScheduleOverrides = withMobXProviderContext(
|
||||
withTheme2(_ScheduleOverrides)
|
||||
) as unknown as React.ComponentClass<Omit<ScheduleOverridesProps, 'store' | 'theme'>>;
|
||||
|
|
|
|||
|
|
@ -417,7 +417,7 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
layerPriorityToShowRotationForm={layerPriorityToShowRotationForm}
|
||||
onShowRotationForm={this.handleShowRotationForm}
|
||||
onShowOverrideForm={this.handleShowOverridesForm}
|
||||
disabled={disabledRotationForm}
|
||||
disabled={Boolean(disabledRotationForm)}
|
||||
filters={filters}
|
||||
onShowShiftSwapForm={!shiftSwapIdToShowForm ? this.handleShowShiftSwapForm : undefined}
|
||||
onSlotClick={shiftSwapIdToShowForm ? this.adjustShiftSwapForm : undefined}
|
||||
|
|
@ -431,9 +431,9 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
onUpdate={this.refreshEventsAndClearPreview}
|
||||
onDelete={this.refreshEventsAndClearPreview}
|
||||
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
|
||||
onShowRotationForm={this.handleShowOverridesForm}
|
||||
disabled={disabledOverrideForm}
|
||||
disableShiftSwaps={disabledShiftSwaps}
|
||||
onShowOverridesForm={this.handleShowOverridesForm}
|
||||
disabled={Boolean(disabledOverrideForm)}
|
||||
disableShiftSwaps={Boolean(disabledShiftSwaps)}
|
||||
shiftStartToShowOverrideForm={shiftStartToShowOverrideForm}
|
||||
shiftEndToShowOverrideForm={shiftEndToShowOverrideForm}
|
||||
onShowShiftSwapForm={!shiftSwapIdToShowForm ? this.handleShowShiftSwapForm : undefined}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const waitForElement = (selector: string) => {
|
||||
export const waitForElement = (selector: string): Promise<Element> => {
|
||||
return new Promise((resolve) => {
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve(document.querySelector(selector));
|
||||
|
|
|
|||
|
|
@ -47,6 +47,16 @@ export function useQueryParams(): URLSearchParams {
|
|||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
export function useResize(onResizeHandler: () => void) {
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onResizeHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResizeHandler);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useDebouncedCallback<A extends any[]>(callback: (...args: A) => void, wait: number) {
|
||||
// track args & timeout handle between calls
|
||||
const argsRef = useRef<A>();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue