diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index 7e6caa2e7..bd3e059a6 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -25,9 +25,7 @@ import { forgivingStringToMillis } from '../utils/dateConfig'; */ export const useEventAction = () => { const queryClient = useQueryClient(); - const eventSettings = useEditorSettings((state) => state.eventSettings); - const defaultPublic = eventSettings.defaultPublic; - const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; + const { defaultPublic, linkPrevious, defaultDuration } = useEditorSettings((state) => state.eventSettings); /** * Calls mutation to add new event @@ -45,11 +43,12 @@ export const useEventAction = () => { after?: string; }; - type EventOptions = BaseOptions & { - defaultPublic?: boolean; - lastEventId?: string; - startTimeIsLastEnd?: boolean; - }; + type EventOptions = BaseOptions & + Partial<{ + defaultPublic: boolean; + linkPrevious: boolean; + lastEventId: string; + }>; /** * Adds an event to rundown @@ -61,27 +60,31 @@ export const useEventAction = () => { // ************* CHECK OPTIONS specific to events if (isOntimeEvent(newEvent)) { const applicationOptions = { - defaultPublic: options?.defaultPublic ?? defaultPublic, - startTimeIsLastEnd: options?.startTimeIsLastEnd ?? startTimeIsLastEnd, - lastEventId: options?.lastEventId, after: options?.after, + defaultPublic: options?.defaultPublic ?? defaultPublic, + lastEventId: options?.lastEventId, + linkPrevious: options?.linkPrevious ?? linkPrevious, }; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this has a value - const rundownData = queryClient.getQueryData(RUNDOWN)!; - const { rundown } = rundownData; - - if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) { + if (applicationOptions.linkPrevious && applicationOptions?.lastEventId) { + newEvent.linkStart = applicationOptions.lastEventId; + } else if (applicationOptions?.lastEventId) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this is a value + const rundownData = queryClient.getQueryData(RUNDOWN)!; + const { rundown } = rundownData; const previousEvent = rundown[applicationOptions.lastEventId]; if (isOntimeEvent(previousEvent)) { newEvent.timeStart = previousEvent.timeEnd; - newEvent.timeEnd = previousEvent.timeEnd; } } if (applicationOptions.defaultPublic) { newEvent.isPublic = true; } + + if (newEvent.duration === undefined && newEvent.timeEnd === undefined) { + newEvent.duration = forgivingStringToMillis(defaultDuration); + } } // handle adding options that concern all event type @@ -95,7 +98,7 @@ export const useEventAction = () => { logAxiosError('Failed adding event', error); } }, - [_addEventMutation, defaultPublic, queryClient, startTimeIsLastEnd], + [_addEventMutation, defaultDuration, defaultPublic, linkPrevious], ); /** diff --git a/apps/client/src/common/stores/editorSettings.ts b/apps/client/src/common/stores/editorSettings.ts index 79d151184..b5e8dafd4 100644 --- a/apps/client/src/common/stores/editorSettings.ts +++ b/apps/client/src/common/stores/editorSettings.ts @@ -4,35 +4,39 @@ import { booleanFromLocalStorage } from '../utils/localStorage'; type EditorSettings = { showQuickEntry: boolean; - startTimeIsLastEnd: boolean; + linkPrevious: boolean; defaultPublic: boolean; + defaultDuration: string; }; type EditorSettingsStore = { eventSettings: EditorSettings; setLocalEventSettings: (newState: EditorSettings) => void; setShowQuickEntry: (showQuickEntry: boolean) => void; - setStartTimeIsLastEnd: (startTimeIsLastEnd: boolean) => void; + setLinkPrevious: (linkPrevious: boolean) => void; setDefaultPublic: (defaultPublic: boolean) => void; + setDefaultDuration: (defaultDuration: string) => void; }; enum EditorSettingsKeys { ShowQuickEntry = 'ontime-show-quick-entry', - StartTimeIsLastEnd = 'ontime-start-is-last-end', + LinkPrevious = 'ontime-link-previous', DefaultPublic = 'ontime-default-public', + DefaultDuration = 'ontime-default-duration', } export const useEditorSettings = create((set) => ({ eventSettings: { showQuickEntry: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, false), - startTimeIsLastEnd: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true), - defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true), + linkPrevious: booleanFromLocalStorage(EditorSettingsKeys.LinkPrevious, true), + defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.DefaultPublic, true), + defaultDuration: localStorage.getItem(EditorSettingsKeys.DefaultDuration) ?? '00:10:00', }, setLocalEventSettings: (value) => set(() => { localStorage.setItem(EditorSettingsKeys.ShowQuickEntry, String(value.showQuickEntry)); - localStorage.setItem(EditorSettingsKeys.StartTimeIsLastEnd, String(value.startTimeIsLastEnd)); + localStorage.setItem(EditorSettingsKeys.LinkPrevious, String(value.linkPrevious)); localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(value.defaultPublic)); return { eventSettings: value }; }), @@ -43,10 +47,10 @@ export const useEditorSettings = create((set) => ({ return { eventSettings: { ...state.eventSettings, showQuickEntry } }; }), - setStartTimeIsLastEnd: (startTimeIsLastEnd) => + setLinkPrevious: (linkPrevious) => set((state) => { - localStorage.setItem(EditorSettingsKeys.StartTimeIsLastEnd, String(startTimeIsLastEnd)); - return { eventSettings: { ...state.eventSettings, startTimeIsLastEnd } }; + localStorage.setItem(EditorSettingsKeys.LinkPrevious, String(linkPrevious)); + return { eventSettings: { ...state.eventSettings, linkPrevious } }; }), setDefaultPublic: (defaultPublic) => @@ -54,4 +58,10 @@ export const useEditorSettings = create((set) => ({ localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(defaultPublic)); return { eventSettings: { ...state.eventSettings, defaultPublic } }; }), + + setDefaultDuration: (defaultDuration) => + set((state) => { + localStorage.setItem(EditorSettingsKeys.DefaultDuration, String(defaultDuration)); + return { eventSettings: { ...state.eventSettings, defaultDuration } }; + }), })); diff --git a/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx b/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx index 26ffb0f3d..29f7d1f3f 100644 --- a/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx +++ b/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx @@ -1,13 +1,18 @@ import { Switch } from '@chakra-ui/react'; +import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import { useEditorSettings } from '../../../../common/stores/editorSettings'; +import { forgivingStringToMillis } from '../../../../common/utils/dateConfig'; import * as Panel from '../PanelUtils'; export default function EditorSettingsForm() { const eventSettings = useEditorSettings((state) => state.eventSettings); const setShowQuickEntry = useEditorSettings((state) => state.setShowQuickEntry); - const setStartTimeIsLastEnd = useEditorSettings((state) => state.setStartTimeIsLastEnd); + const setLinkPrevious = useEditorSettings((state) => state.setLinkPrevious); const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic); + const setDefaultDuration = useEditorSettings((state) => state.setDefaultDuration); + + const durationInMs = forgivingStringToMillis(eventSettings.defaultDuration); return ( @@ -29,14 +34,26 @@ export default function EditorSettingsForm() { setStartTimeIsLastEnd(event.target.checked)} + defaultChecked={eventSettings.linkPrevious} + onChange={(event) => setLinkPrevious(event.target.checked)} + /> + + + + + name='defaultDuration' + submitHandler={(_field, value) => setDefaultDuration(value)} + time={durationInMs} + placeholder='00:10:00' /> diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index ab239c77d..5be626f4b 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -38,7 +38,7 @@ export default function Rundown({ data }: RundownProps) { const { addEvent, reorderEvent } = useEventAction(); const eventSettings = useEditorSettings((state) => state.eventSettings); const defaultPublic = eventSettings.defaultPublic; - const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; + const linkPrevious = eventSettings.linkPrevious; const showQuickEntry = eventSettings.showQuickEntry; // cursor @@ -74,17 +74,17 @@ export default function Rundown({ data }: RundownProps) { type: SupportedEvent.Event, }; const options = { - defaultPublic, - startTimeIsLastEnd, - lastEventId: cursor, after: cursor, + defaultPublic, + lastEventId: cursor, + linkPrevious, }; addEvent(newEvent, options); } else { addEvent({ type }, { after: cursor }); } }, - [addEvent, rundown, defaultPublic, startTimeIsLastEnd], + [addEvent, rundown, defaultPublic, linkPrevious], ); // Handle keyboard shortcuts diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 0c58caa0d..61a36644d 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -52,7 +52,7 @@ export default function RundownEntry(props: RundownEntryProps) { const eventSettings = useEditorSettings((state) => state.eventSettings); const defaultPublic = eventSettings.defaultPublic; - const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; + const linkPrevious = eventSettings.linkPrevious; const removeOpenEvent = useCallback(() => { if (selectedEvents.has(data.id)) { @@ -63,7 +63,7 @@ export default function RundownEntry(props: RundownEntryProps) { if (cursor === data.id) { setCursor(null); } - }, [clearSelectedEvents, cursor, data.id, selectedEvents, setCursor]); + }, [selectedEvents, data.id, cursor, clearSelectedEvents, setCursor]); // Create / delete new events type FieldValue = { @@ -76,10 +76,10 @@ export default function RundownEntry(props: RundownEntryProps) { case 'event': { const newEvent = { type: SupportedEvent.Event }; const options = { - startTimeIsLastEnd, + after: data.id, defaultPublic, lastEventId: previousEventId, - after: data.id, + linkPrevious, }; return addEvent(newEvent, options); } diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx index e04ee1497..f85cf6e91 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx @@ -23,26 +23,24 @@ const QuickAddBlock = (props: QuickAddBlockProps) => { const { addEvent } = useEventAction(); const { emitError } = useEmitLog(); - const doStartTime = useRef(null); + const doLinkPrevious = useRef(null); const doPublic = useRef(null); - const eventSettings = useEditorSettings((state) => state.eventSettings); - const defaultPublic = eventSettings.defaultPublic; - const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; + const { defaultPublic, linkPrevious } = useEditorSettings((state) => state.eventSettings); const handleCreateEvent = useCallback( (eventType: SupportedEvent) => { switch (eventType) { case 'event': { - const isPublicOption = doPublic?.current?.checked; - const startTimeIsLastEndOption = doStartTime?.current?.checked; + const defaultPublic = doPublic?.current?.checked; + const linkPrevious = doLinkPrevious?.current?.checked; const newEvent = { type: SupportedEvent.Event }; const options = { - defaultPublic: isPublicOption, - startTimeIsLastEnd: startTimeIsLastEndOption, - lastEventId: previousEventId, after: previousEventId, + defaultPublic, + lastEventId: previousEventId, + linkPrevious, }; addEvent(newEvent, options); break; @@ -115,8 +113,8 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
- - Start time is last end + + Link to previous Event is public diff --git a/e2e/tests/features/203-delay-block.spec.ts b/e2e/tests/features/203-delay-block.spec.ts index 54ad3d7a0..6b5030be7 100644 --- a/e2e/tests/features/203-delay-block.spec.ts +++ b/e2e/tests/features/203-delay-block.spec.ts @@ -67,9 +67,7 @@ test('delays are show correctly', async ({ page }) => { await page.getByTestId('block__title').click(); await page.getByTestId('block__title').fill('test'); await page.getByTestId('block__title').press('Enter'); - - await page.locator('#event-block').getByText('1').click({ button: 'right' }); - await page.getByRole('menuitem', { name: 'Toggle public' }).click(); + await expect(page.getByTestId('entry-1').locator('#block-status')).toHaveAttribute('data-ispublic', 'true'); // add a delay await page.getByRole('button', { name: 'Rundown menu' }).click(); diff --git a/e2e/tests/features/204-editor-crud.spec.ts b/e2e/tests/features/204-editor-crud.spec.ts index aba543da8..e37b4871f 100644 --- a/e2e/tests/features/204-editor-crud.spec.ts +++ b/e2e/tests/features/204-editor-crud.spec.ts @@ -18,12 +18,12 @@ test('CRUD operations on the rundown', async ({ page }) => { // test quick add options - start is last end await page.getByTestId('entry-2').getByTestId('time-input-duration').fill('20m'); - await page.getByText('Start time is last end').click(); await page.getByTestId('quick-add-event').click(); - expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain('00:20:00'); + await expect(page.getByLabel('Link to previous')).toBeChecked(); + expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain('00:30:00'); // test quick add options - event is public - await page.locator('label').filter({ hasText: 'Event is public' }).click(); + await expect(page.getByLabel('Event is public')).toBeChecked(); await page.getByTestId('quick-add-event').click(); await expect(page.getByTestId('entry-4').locator('#block-status')).toHaveAttribute('data-ispublic', 'true'); diff --git a/e2e/tests/features/205-operator.spec.ts b/e2e/tests/features/205-operator.spec.ts index a0c1e01a9..8bdf603e8 100644 --- a/e2e/tests/features/205-operator.spec.ts +++ b/e2e/tests/features/205-operator.spec.ts @@ -13,14 +13,12 @@ test('smoke test operator', async ({ page }) => { await page.getByTestId('time-input-duration').fill('1m'); await page.getByTestId('time-input-duration').press('Enter'); - await page.getByText('Start time is last end').click(); await page.getByTestId('quick-add-event').click(); await page.getByTestId('entry-2').getByTestId('lock__duration').click(); await page.getByTestId('entry-2').getByTestId('time-input-duration').fill('1m'); await page.getByTestId('entry-2').getByTestId('time-input-duration').press('Enter'); await page.getByTestId('entry-2').getByTestId('time-input-duration').press('Enter'); - await page.getByText('Start time is last end').click(); await page.getByTestId('quick-add-event').click(); await page.getByTestId('entry-3').getByTestId('lock__duration').click(); await page.getByTestId('entry-3').getByTestId('time-input-duration').fill('1m'); diff --git a/package.json b/package.json index 6bd553373..11a7290ba 100644 --- a/package.json +++ b/package.json @@ -41,14 +41,14 @@ "cleanup": "rm -rf node_modules && rm -rf **/node_modules && rm -rf **/**/node_modules" }, "devDependencies": { - "@playwright/test": "^1.34.3", + "@playwright/test": "^1.42.1", "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "cross-env": "^7.0.3", "eslint": "^8.53.0", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-playwright": "^0.12.0", + "eslint-plugin-playwright": "^1.5.2", "husky": "^8.0.3", "lint-staged": "^15.1.0", "prettier": "^3.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9691d090..e46e74f0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@playwright/test': - specifier: ^1.34.3 - version: 1.34.3 + specifier: ^1.42.1 + version: 1.42.1 '@types/node': specifier: ^18.11.18 version: 18.11.18 @@ -30,8 +30,8 @@ importers: specifier: ^9.0.0 version: 9.0.0(eslint@8.53.0) eslint-plugin-playwright: - specifier: ^0.12.0 - version: 0.12.0(eslint@8.53.0) + specifier: ^1.5.2 + version: 1.5.2(eslint@8.53.0) husky: specifier: ^8.0.3 version: 8.0.3 @@ -700,8 +700,8 @@ packages: regenerator-runtime: 0.13.11 dev: false - /@babel/runtime@7.23.9: - resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} + /@babel/runtime@7.24.0: + resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 @@ -2471,15 +2471,12 @@ packages: tslib: 2.6.2 dev: true - /@playwright/test@1.34.3: - resolution: {integrity: sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==} - engines: {node: '>=14'} + /@playwright/test@1.42.1: + resolution: {integrity: sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==} + engines: {node: '>=16'} hasBin: true dependencies: - '@types/node': 18.11.18 - playwright-core: 1.34.3 - optionalDependencies: - fsevents: 2.3.2 + playwright: 1.42.1 dev: true /@popperjs/core@2.11.8: @@ -3080,7 +3077,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.23.5 - '@babel/runtime': 7.23.9 + '@babel/runtime': 7.24.0 '@types/aria-query': 5.0.4 aria-query: 5.1.3 chalk: 4.1.2 @@ -5309,16 +5306,18 @@ packages: - typescript dev: true - /eslint-plugin-playwright@0.12.0(eslint@8.53.0): - resolution: {integrity: sha512-KXuzQjVzca5irMT/7rvzJKsVDGbQr43oQPc8i+SLEBqmfrTxlwMwRqfv9vtZqh4hpU0jmrnA/EOfwtls+5QC1w==} + /eslint-plugin-playwright@1.5.2(eslint@8.53.0): + resolution: {integrity: sha512-TMzLrLGQMccngU8GogtzIc9u5RzXGnfsQEUjLfEfshINuVR2fS4SHfDtU7xYP90Vwm5vflHECf610KTdGvO53w==} + engines: {node: '>=16.6.0'} peerDependencies: - eslint: '>=7' - eslint-plugin-jest: '>=24' + eslint: '>=8.40.0' + eslint-plugin-jest: '>=25' peerDependenciesMeta: eslint-plugin-jest: optional: true dependencies: eslint: 8.53.0 + globals: 13.24.0 dev: true /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3): @@ -6098,6 +6097,13 @@ packages: type-fest: 0.20.2 dev: true + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + /globalthis@1.0.3: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} @@ -7687,12 +7693,22 @@ packages: pathe: 1.1.1 dev: true - /playwright-core@1.34.3: - resolution: {integrity: sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==} - engines: {node: '>=14'} + /playwright-core@1.42.1: + resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} + engines: {node: '>=16'} hasBin: true dev: true + /playwright@1.42.1: + resolution: {integrity: sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.42.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'}