From 6f634c36f9cfbb76b95ccf456813e4b10278d6d5 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Mon, 26 Dec 2022 09:40:33 +0100 Subject: [PATCH] v2 alpha 3 (#272) * style: correct background colours * chore: add folder resolutions to vite * refactor: add auto refetch to HTTP APIs * fix: view settings override endpoint * refactor: memoize callbacks * fix: reorder reaching wrong data adapter * refactor: memoize callbacks * style: presentation cleanup * fix(delete): prevent flow with deleted event in playback * refactor: convert to typescript * refactor: small fixes and typescript conversion * ux: improve feedback on local changes * refactor: cleanup props --- client/src/common/api/eventsApi.ts | 10 +- .../input/colour-input/ColourInput.tsx | 9 +- client/src/common/components/state/Empty.tsx | 2 + client/src/common/hooks-query/useAliases.ts | 2 + client/src/common/hooks-query/useEvent.ts | 2 + client/src/common/hooks-query/useInfo.ts | 2 + .../src/common/hooks-query/useOscSettings.ts | 2 + client/src/common/hooks-query/useRundown.ts | 2 + client/src/common/hooks-query/useSettings.ts | 2 + .../src/common/hooks-query/useUserFields.ts | 2 + .../src/common/hooks-query/useViewSettings.ts | 2 + .../{useEventAction.js => useEventAction.ts} | 170 ++++++++++++------ client/src/common/hooks/useFullscreen.js | 20 ++- client/src/common/hooks/useSocket.ts | 11 +- client/src/common/models/EventTypes.ts | 15 +- .../features/control/playback/Transport.tsx | 4 +- .../src/features/event-editor/EventEditor.tsx | 5 +- .../src/features/modals/AppSettingsModal.jsx | 22 ++- .../features/modals/ViewsSettingsModal.jsx | 5 +- .../rundown/{Rundown.jsx => Rundown.tsx} | 117 +++++++----- client/src/features/rundown/RundownEntry.tsx | 38 ++-- .../event-block/EventBlock.module.scss | 4 + .../rundown/event-block/EventBlock.tsx | 10 +- .../event-block/composite/BlockActionMenu.tsx | 17 +- .../rundown/quick-add-block/QuickAddBlock.tsx | 43 +++-- client/src/ontimeConfig.ts | 3 + client/src/theme/_v2Styles.scss | 2 +- client/src/theme/ontimeMenu.ts | 1 + client/vite.config.js | 8 +- server/src/services/rundownService.js | 2 +- 30 files changed, 354 insertions(+), 180 deletions(-) rename client/src/common/hooks/{useEventAction.js => useEventAction.ts} (53%) rename client/src/features/rundown/{Rundown.jsx => Rundown.tsx} (71%) diff --git a/client/src/common/api/eventsApi.ts b/client/src/common/api/eventsApi.ts index 7d39c4f12..e0a8b0055 100644 --- a/client/src/common/api/eventsApi.ts +++ b/client/src/common/api/eventsApi.ts @@ -25,7 +25,7 @@ export async function requestPostEvent(data: OntimeRundownEntry) { * @description HTTP request to put new event * @return {Promise} */ -export async function requestPutEvent(data: OntimeRundownEntry) { +export async function requestPutEvent(data: Partial) { return axios.put(rundownURL, data); } @@ -37,11 +37,17 @@ export async function requestPatchEvent(data: OntimeRundownEntry) { return axios.patch(rundownURL, data); } + +export type ReorderEntry = { + eventId: string, + from: number, + to: number, +} /** * @description HTTP request to reorder events * @return {Promise} */ -export async function requestReorderEvent(data: OntimeRundownEntry) { +export async function requestReorderEvent(data: ReorderEntry) { return axios.patch(`${rundownURL}/reorder`, data); } diff --git a/client/src/common/components/input/colour-input/ColourInput.tsx b/client/src/common/components/input/colour-input/ColourInput.tsx index b4f910406..b9040352f 100644 --- a/client/src/common/components/input/colour-input/ColourInput.tsx +++ b/client/src/common/components/input/colour-input/ColourInput.tsx @@ -1,14 +1,17 @@ import { Input } from '@chakra-ui/react'; +import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor'; + import style from './ColourInput.module.scss'; interface ColourInputProps { value: string; - handleChange: (newValue: string) => void; + name: EventEditorSubmitActions; + handleChange: (newValue: EventEditorSubmitActions, name: string) => void; } export default function ColourInput(props: ColourInputProps) { - const { value, handleChange } = props; + const { value, name, handleChange } = props; return ( handleChange(event.target.value)} + onChange={(event) => handleChange(name, event.target.value)} /> ); } diff --git a/client/src/common/components/state/Empty.tsx b/client/src/common/components/state/Empty.tsx index 7b3a42472..cf256a01d 100644 --- a/client/src/common/components/state/Empty.tsx +++ b/client/src/common/components/state/Empty.tsx @@ -1,9 +1,11 @@ +import { CSSProperties } from 'react'; import { ReactComponent as Emptyimage } from 'assets/images/empty.svg'; import style from './Empty.module.scss'; interface EmptyProps { text: string; + style: CSSProperties; } export default function Empty(props: EmptyProps) { diff --git a/client/src/common/hooks-query/useAliases.ts b/client/src/common/hooks-query/useAliases.ts index 8ee0bc0c3..270addff1 100644 --- a/client/src/common/hooks-query/useAliases.ts +++ b/client/src/common/hooks-query/useAliases.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { ALIASES } from '../api/apiConstants'; import { getAliases } from '../api/ontimeApi'; @@ -15,6 +16,7 @@ export default function useAliases() { placeholderData: [], retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchIntervalSlow, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks-query/useEvent.ts b/client/src/common/hooks-query/useEvent.ts index 0c3595685..4248bd47f 100644 --- a/client/src/common/hooks-query/useEvent.ts +++ b/client/src/common/hooks-query/useEvent.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { EVENT_TABLE } from '../api/apiConstants'; import { fetchEvent } from '../api/eventApi'; import { eventDataPlaceholder } from '../models/EventData.type'; @@ -16,6 +17,7 @@ export default function useEvent() { placeholderData: eventDataPlaceholder, retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchIntervalSlow, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks-query/useInfo.ts b/client/src/common/hooks-query/useInfo.ts index e17e881ca..82cbf5ade 100644 --- a/client/src/common/hooks-query/useInfo.ts +++ b/client/src/common/hooks-query/useInfo.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { APP_INFO } from '../api/apiConstants'; import { getInfo } from '../api/ontimeApi'; import { ontimePlaceholderInfo } from '../models/Info.types'; @@ -16,6 +17,7 @@ export default function useInfo() { placeholderData: ontimePlaceholderInfo, retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchIntervalSlow, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks-query/useOscSettings.ts b/client/src/common/hooks-query/useOscSettings.ts index dc44252c8..059e4eedd 100644 --- a/client/src/common/hooks-query/useOscSettings.ts +++ b/client/src/common/hooks-query/useOscSettings.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { OSC_SETTINGS } from '../api/apiConstants'; import { getOSC } from '../api/ontimeApi'; import { oscPlaceholderSettings } from '../models/OscSettings.type'; @@ -16,6 +17,7 @@ export default function useOscSettings() { placeholderData: oscPlaceholderSettings, retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchIntervalSlow, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks-query/useRundown.ts b/client/src/common/hooks-query/useRundown.ts index b15fe71fd..d09f4c485 100644 --- a/client/src/common/hooks-query/useRundown.ts +++ b/client/src/common/hooks-query/useRundown.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchInterval } from '../../ontimeConfig'; import { RUNDOWN_TABLE } from '../api/apiConstants'; import { fetchRundown } from '../api/eventsApi'; @@ -15,6 +16,7 @@ export default function useRundown() { placeholderData: [], retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchInterval, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks-query/useSettings.ts b/client/src/common/hooks-query/useSettings.ts index 35f27762f..210707732 100644 --- a/client/src/common/hooks-query/useSettings.ts +++ b/client/src/common/hooks-query/useSettings.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { APP_SETTINGS } from '../api/apiConstants'; import { getSettings } from '../api/ontimeApi'; import { ontimePlaceholderSettings } from '../models/OntimeSettings.type'; @@ -16,6 +17,7 @@ export default function useSettings() { placeholderData: ontimePlaceholderSettings, retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchIntervalSlow, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks-query/useUserFields.ts b/client/src/common/hooks-query/useUserFields.ts index 2c0f8078b..d3ea3ec61 100644 --- a/client/src/common/hooks-query/useUserFields.ts +++ b/client/src/common/hooks-query/useUserFields.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchInterval } from '../../ontimeConfig'; import { USERFIELDS } from '../api/apiConstants'; import { getUserFields } from '../api/ontimeApi'; import { userFieldsPlaceholder } from '../models/UserFields.type'; @@ -16,6 +17,7 @@ export default function useUserFields() { placeholderData: userFieldsPlaceholder, retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchInterval, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks-query/useViewSettings.ts b/client/src/common/hooks-query/useViewSettings.ts index 6c4571f1d..0fbf8c3a0 100644 --- a/client/src/common/hooks-query/useViewSettings.ts +++ b/client/src/common/hooks-query/useViewSettings.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { VIEW_SETTINGS } from '../api/apiConstants'; import { getView } from '../api/ontimeApi'; import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; @@ -16,6 +17,7 @@ export default function useViewSettings() { placeholderData: viewsSettingsPlaceholder, retry: 5, retryDelay: attempt => attempt * 2500, + refetchInterval: queryRefetchIntervalSlow, }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks/useEventAction.js b/client/src/common/hooks/useEventAction.ts similarity index 53% rename from client/src/common/hooks/useEventAction.js rename to client/src/common/hooks/useEventAction.ts index ed2e37a6a..4b125bd33 100644 --- a/client/src/common/hooks/useEventAction.js +++ b/client/src/common/hooks/useEventAction.ts @@ -1,8 +1,11 @@ import { useCallback, useContext } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import axios, { AxiosError } from 'axios'; +import { useAtomValue } from 'jotai'; import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants'; import { + ReorderEntry, requestApplyDelay, requestDelete, requestDeleteAll, @@ -10,7 +13,9 @@ import { requestPutEvent, requestReorderEvent, } from '../api/eventsApi'; +import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings'; import { LoggingContext } from '../context/LoggingContext'; +import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from '../models/EventTypes'; /** * @description Set of utilities for events @@ -18,9 +23,11 @@ import { LoggingContext } from '../context/LoggingContext'; export const useEventAction = () => { const queryClient = useQueryClient(); const { emitError } = useContext(LoggingContext); + const defaultPublic = useAtomValue(defaultPublicAtom); + const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); /** - * @description Calls mutation to add new event + * Calls mutation to add new event * @private */ const _addEventMutation = useMutation(requestPostEvent, { @@ -31,40 +38,72 @@ export const useEventAction = () => { }, }); + type AddOptions = { + defaultPublic?: boolean; + startTimeIsLastEnd?: boolean; + lastEventId?: string; + after?: string; + } + /** - * @description Adds new event to list - * @param {object} event - Event to be added - * @param {object} [options] - Event options + * Adds an event to rundown */ const addEvent = useCallback( - async (event, options) => { - const newEvent = { ...event }; + async (event: Partial, options?: AddOptions) => { + const newEvent: Partial = { ...event }; + // ************* CHECK OPTIONS // there is an option to pass an index of an array to use as start time - if (typeof options?.startIsLastEnd !== 'undefined') { - const events = queryClient.getQueryData(RUNDOWN_TABLE); - const previousEvent = events.find((event) => event.id === options.startIsLastEnd); - newEvent.timeStart = previousEvent.timeEnd || 0; - } + // only events have options + if (newEvent.type === SupportedEvent.Event) { + const applicationOptions = { + defaultPublic: options?.defaultPublic ?? defaultPublic, + startTimeIsLastEnd: options?.startTimeIsLastEnd ?? startTimeIsLastEnd, + lastEventId: options?.lastEventId, + after: options?.after, + }; - // hard coding duration value to be as expected for now - // this until timeOptions gets implemented - if (newEvent.type === 'event') { - newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0; + // hard coding duration value to be as expected for now + // this until timeOptions gets implemented + if (typeof newEvent?.timeStart !== 'undefined' && typeof newEvent.timeEnd !== 'undefined') { + newEvent.duration = Math.max(0, newEvent?.timeEnd - newEvent?.timeStart) || 0; + } + + if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) { + console.log('debug got here', applicationOptions.startTimeIsLastEnd, typeof applicationOptions.startTimeIsLastEnd); + const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown; + const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId); + if (typeof previousEvent !== 'undefined' && previousEvent.type === 'event') { + newEvent.timeStart = previousEvent.timeEnd; + } + } + + if (applicationOptions.defaultPublic) { + newEvent.isPublic = true; + } + + if (applicationOptions?.after) { + newEvent.after = applicationOptions.after; + } } try { + // @ts-expect-error we know that the event here is one of the defined types await _addEventMutation.mutateAsync(newEvent); } catch (error) { - emitError(`Error fetching data: ${error.message}`); + if(!axios.isAxiosError(error)){ + emitError(`Error fetching data: ${(error as AxiosError).message}`); + } else { + emitError(`Error fetching data: ${error}`); + } } }, - [_addEventMutation, emitError, queryClient], + [_addEventMutation, defaultPublic, emitError, queryClient, startTimeIsLastEnd], ); /** - * @description Calls mutation to update existing event + * Calls mutation to update existing event * @private */ const _updateEventMutation = useMutation(requestPutEvent, { @@ -84,33 +123,37 @@ export const useEventAction = () => { }, // Mutation fails, rollback undoes optimist update - onError: (error, newEvent, context) => { - queryClient.setQueryData([RUNDOWN_TABLE_KEY, context.newEvent.id], context.previousEvent); + onError: (_error, _newEvent, context) => { + queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure - onSettled: async (newEvent) => { - await queryClient.invalidateQueries([RUNDOWN_TABLE_KEY, newEvent.id]); + onSettled: async () => { + await queryClient.invalidateQueries([RUNDOWN_TABLE_KEY]); }, }); /** - * @description Updates existing event - * @param {object} event - Event to be added + * Updates existing event */ const updateEvent = useCallback( - async (event) => { + async (event: Partial) => { try { await _updateEventMutation.mutateAsync(event); } catch (error) { - emitError(`Error updating event: ${error.message}`); + if(!axios.isAxiosError(error)){ + emitError(`Error updating event: ${(error as AxiosError).message}`); + } else { + emitError(`Error updating event: ${error}`); + } + } }, [_updateEventMutation, emitError], ); /** - * @description Calls mutation to delete an event + * Calls mutation to delete an event * @private */ const _deleteEventMutation = useMutation(requestDelete, { @@ -122,7 +165,7 @@ export const useEventAction = () => { // Snapshot the previous value const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE); - const filtered = [...previousEvents].filter((e) => e.id !== eventId); + const filtered = [...(previousEvents as OntimeRundown)].filter((e) => e.id !== eventId); // optimistically update object queryClient.setQueryData(RUNDOWN_TABLE, filtered); @@ -132,8 +175,8 @@ export const useEventAction = () => { }, // Mutation fails, rollback undoes optimist update - onError: (error, eventId, context) => { - queryClient.setQueryData(RUNDOWN_TABLE, context.previousEvents); + onError: (_error, _eventId, context) => { + queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure @@ -143,22 +186,25 @@ export const useEventAction = () => { }); /** - * @description Deletes an event form the list - * @param {object} eventId - Event to be deleted + * Deletes an event form the list */ const deleteEvent = useCallback( - async (eventId) => { + async (eventId: string) => { try { await _deleteEventMutation.mutateAsync(eventId); - } catch (error) { - emitError(`Error deleting event: ${error.message}`); + } catch (error) { + if(!axios.isAxiosError(error)){ + emitError(`Error deleting event: ${(error as AxiosError).message}`); + } else { + emitError(`Error deleting event: ${error}`); + } } }, [_deleteEventMutation, emitError], ); /** - * @description Calls mutation to delete all events + * Calls mutation to delete all events * @private */ const _deleteAllEventsMutation = useMutation(requestDeleteAll, { @@ -170,18 +216,16 @@ export const useEventAction = () => { // Snapshot the previous value const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE); - const clear = []; - // optimistically update object - queryClient.setQueryData(RUNDOWN_TABLE, clear); + queryClient.setQueryData(RUNDOWN_TABLE, []); // Return a context with the previous and new events return { previousEvents }; }, // Mutation fails, rollback undos optimist update - onError: (error, eventId, context) => { - queryClient.setQueryData(RUNDOWN_TABLE, context.previousEvents); + onError: (_error, _eventId, context) => { + queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure @@ -191,18 +235,22 @@ export const useEventAction = () => { }); /** - * @description Deletes all events from list + * Deletes all events from list */ const deleteAllEvents = useCallback(async () => { try { await _deleteAllEventsMutation.mutateAsync(); } catch (error) { - emitError(`Error deleting events: ${error.message}`); + if(!axios.isAxiosError(error)){ + emitError(`Error deleting events: ${(error as AxiosError).message}`); + } else { + emitError(`Error deleting events: ${error}`); + } } }, [_deleteAllEventsMutation, emitError]); /** - * @description Calls mutation to apply a delay + * Calls mutation to apply a delay * @private */ const _applyDelayMutation = useMutation(requestApplyDelay, { @@ -213,22 +261,25 @@ export const useEventAction = () => { }); /** - * @description Applies a given delay - * @param {object} delayEventId - Id of delay to be applied + * Applies a given delay block */ const applyDelay = useCallback( - async (delayEventId) => { + async (delayEventId: string) => { try { await _applyDelayMutation.mutateAsync(delayEventId); } catch (error) { - emitError(`Error applying delay: ${error.message}`); + if(!axios.isAxiosError(error)){ + emitError(`Error applying delay: ${(error as AxiosError).message}`); + } else { + emitError(`Error applying delay: ${error}`); + } } }, [_applyDelayMutation, emitError], ); /** - * @description Calls mutation to reorder an event + * Calls mutation to reorder an event * @private */ const _reorderEventMutation = useMutation(requestReorderEvent, { @@ -240,7 +291,7 @@ export const useEventAction = () => { // Snapshot the previous value const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE); - const e = [...previousEvents]; + const e = [...(previousEvents as OntimeRundown)]; const [reorderedItem] = e.splice(data.from, 1); e.splice(data.to, 0, reorderedItem); @@ -252,8 +303,8 @@ export const useEventAction = () => { }, // Mutation fails, rollback undoes optimist update - onError: (error, eventId, context) => { - queryClient.setQueryData(RUNDOWN_TABLE, context.previousEvents); + onError: (_error, _eventId, context) => { + queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure @@ -263,22 +314,23 @@ export const useEventAction = () => { }); /** - * @description Reorders a given event - * @param {string} eventID - ID of event to reorder - * @param {number} from - Current index - * @param {number} to - New Index + * Reorders a given event */ const reorderEvent = useCallback( - async (eventId, from, to) => { + async (eventId: string, from: number, to: number) => { try { - const reorderObject = { + const reorderObject: ReorderEntry = { eventId: eventId, from: from, to: to, }; await _reorderEventMutation.mutateAsync(reorderObject); } catch (error) { - emitError(`Error re-ordering event: ${error.message}`); + if(!axios.isAxiosError(error)){ + emitError(`Error re-ordering event: ${(error as AxiosError).message}`); + } else { + emitError(`Error re-ordering event: ${error}`); + } } }, [_reorderEventMutation, emitError], diff --git a/client/src/common/hooks/useFullscreen.js b/client/src/common/hooks/useFullscreen.js index 94dda7e87..45a5f2a22 100644 --- a/client/src/common/hooks/useFullscreen.js +++ b/client/src/common/hooks/useFullscreen.js @@ -14,17 +14,29 @@ export default function useFullscreen() { return () => { document.removeEventListener('fullscreenchange', handleChange, { passive: true }); document.removeEventListener('resize', handleChange, { passive: true }); - }; }, []); + }; + }, []); const toggleFullScreen = useCallback(() => { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen(); + if (!document.fullscreenElement && !document.webkitIsFullScreen) { + // Fullscreen mode is not active, so we can enter fullscreen mode + if (document.documentElement.requestFullscreen) { + // Standard fullscreen API is supported + document.documentElement.requestFullscreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + // iOS Safari fullscreen API is supported + document.documentElement.webkitRequestFullscreen(); + } } else { + // Fullscreen mode is active, so we can exit fullscreen mode if (document.exitFullscreen) { + // Standard fullscreen API is supported document.exitFullscreen(); + } else if (document.webkitCancelFullscreen) { + // iOS Safari fullscreen API is supported + document.webkitCancelFullscreen(); } } - setFullScreen(document.fullscreenElement); }, []); return { isFullScreen, toggleFullScreen }; diff --git a/client/src/common/hooks/useSocket.ts b/client/src/common/hooks/useSocket.ts index 7d7ff117c..33f7cced0 100644 --- a/client/src/common/hooks/useSocket.ts +++ b/client/src/common/hooks/useSocket.ts @@ -10,6 +10,7 @@ import { FEAT_RUNDOWN, TIMER, } from '../api/apiConstants'; +import { Playstate } from '../models/OntimeTypes'; function createSocketHook(key: string, defaultValue: T | null = null) { subscribeOnce(key, (data) => queryClient.setQueryData([key], data)); @@ -21,8 +22,14 @@ function createSocketHook(key: string, defaultValue: T | null = null) { return () => useQuery({ queryKey: [key], queryFn: fetcher, placeholderData: defaultValue }); } -const emptyRundown = { - selectEventId: null, +interface IRundown { + selectedEventId: string | null; + nextEventId: string | null; + playback: Playstate | null; +} + +const emptyRundown: IRundown = { + selectedEventId: null, nextEventId: null, playback: null, }; diff --git a/client/src/common/models/EventTypes.ts b/client/src/common/models/EventTypes.ts index ae6f0ba22..b03da4f0f 100644 --- a/client/src/common/models/EventTypes.ts +++ b/client/src/common/models/EventTypes.ts @@ -1,22 +1,27 @@ -export type EventTypes = 'event' | 'delay' | 'block'; +export enum SupportedEvent { + Event = 'event', + Delay = 'delay', + Block = 'block' +} export interface OntimeBaseEvent { - type: EventTypes; + type: SupportedEvent; id: string; + after?: string; // used when creating an event to indicate its position in rundown } export type OntimeDelay = OntimeBaseEvent & { - type: 'delay'; + type: SupportedEvent.Delay; duration: number; revision: number; } export type OntimeBlock = OntimeBaseEvent & { - type: 'block'; + type: SupportedEvent.Block; } export type OntimeEvent = OntimeBaseEvent & { - type: 'event'; + type: SupportedEvent.Event; title: string, subtitle: string, presenter: string, diff --git a/client/src/features/control/playback/Transport.tsx b/client/src/features/control/playback/Transport.tsx index 7a647a7f2..88e38309a 100644 --- a/client/src/features/control/playback/Transport.tsx +++ b/client/src/features/control/playback/Transport.tsx @@ -43,7 +43,7 @@ export default function Transport(props: TransportProps) { setPlayback.reload()} - disabled={selectedId == null || isRolling || noEvents} + disabled={!selectedId || noEvents} > @@ -51,7 +51,7 @@ export default function Transport(props: TransportProps) { setPlayback.stop()} - disabled={(selectedId == null && !isRolling) || noEvents} + disabled={!selectedId} theme='stop' > diff --git a/client/src/features/event-editor/EventEditor.tsx b/client/src/features/event-editor/EventEditor.tsx index 911cc1a8f..0971f5e06 100644 --- a/client/src/features/event-editor/EventEditor.tsx +++ b/client/src/features/event-editor/EventEditor.tsx @@ -117,7 +117,7 @@ export default function EventEditor() { return (
-
{`Event ${'not yet'} | Event ID ${event.id}`}
+
{`Event ID ${event.id}`}
{`/ontime/gotoid/${event.id}`}
@@ -207,7 +207,8 @@ export default function EventEditor() {
handleSubmit('colour', value)} + name='colour' + handleChange={handleSubmit} />
diff --git a/client/src/features/modals/ViewsSettingsModal.jsx b/client/src/features/modals/ViewsSettingsModal.jsx index c03be820d..5b2ab4f92 100644 --- a/client/src/features/modals/ViewsSettingsModal.jsx +++ b/client/src/features/modals/ViewsSettingsModal.jsx @@ -6,7 +6,7 @@ import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInforma import { postView } from '../../common/api/ontimeApi'; import EnableBtn from '../../common/components/buttons/EnableBtn'; import { LoggingContext } from '../../common/context/LoggingContext'; -import useSettings from '../../common/hooks-query/useSettings'; +import useViewSettings from '../../common/hooks-query/useViewSettings'; import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type'; import { openLink } from '../../common/utils/linkUtils'; @@ -15,7 +15,8 @@ import SubmitContainer from './SubmitContainer'; import style from './Modals.module.scss'; export default function ViewsSettingsModal() { - const { data, status, refetch } = useSettings(); + const { data, status, refetch } = useViewSettings(); + const { emitError } = useContext(LoggingContext); const [formData, setFormData] = useState(viewsSettingsPlaceholder); const [changed, setChanged] = useState(false); diff --git a/client/src/features/rundown/Rundown.jsx b/client/src/features/rundown/Rundown.tsx similarity index 71% rename from client/src/features/rundown/Rundown.jsx rename to client/src/features/rundown/Rundown.tsx index 1962d3883..b0012f114 100644 --- a/client/src/features/rundown/Rundown.jsx +++ b/client/src/features/rundown/Rundown.tsx @@ -11,6 +11,7 @@ import Empty from 'common/components/state/Empty'; import { CursorContext } from 'common/context/CursorContext'; import { useEventAction } from 'common/hooks/useEventAction'; import { useRundownEditor } from 'common/hooks/useSocket'; +import { OntimeRundown, SupportedEvent } from 'common/models/EventTypes'; import { cloneEvent } from 'common/utils/eventsManager'; import { useAtomValue } from 'jotai'; import PropTypes from 'prop-types'; @@ -20,21 +21,27 @@ import RundownEntry from './RundownEntry'; import style from './Rundown.module.scss'; -export default function Rundown(props) { +interface RundownProps { + entries: OntimeRundown; +} + +export default function Rundown(props: RundownProps) { const { entries } = props; - // Todo: add selectedId and nextId to rundown editor hook const { data } = useRundownEditor(); const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const defaultPublic = useAtomValue(defaultPublicAtom); const { addEvent, reorderEvent } = useEventAction(); - const cursorRef = createRef(); + const cursorRef = createRef(); const showQuickEntry = useAtomValue(showQuickEntryAtom); const insertAtCursor = useCallback( - (type, cursor) => { + (type: SupportedEvent | 'clone', cursor: number) => { if (cursor === -1) { + if (type === 'clone') { + return; + } addEvent({ type }); } else { const previousEvent = entries?.[cursor]; @@ -43,22 +50,23 @@ export default function Rundown(props) { // prevent adding two non-event blocks consecutively const isPreviousDifferent = previousEvent?.type !== type; const isNextDifferent = nextEvent?.type !== type; - if (type === 'clone' && previousEvent) { + if (type === 'clone' && previousEvent?.type === SupportedEvent.Event) { const newEvent = cloneEvent(previousEvent); newEvent.after = previousEvent.id; addEvent(newEvent); - } else if (type === 'event') { + } else if (type === SupportedEvent.Event) { const newEvent = { - type: 'event', - after: previousEvent.id, - isPublic: defaultPublic, + type: SupportedEvent.Event, }; const options = { - startIsLastEnd: startTimeIsLastEnd ? previousEvent.id : undefined, + defaultPublic: defaultPublic, + startTimeIsLastEnd: startTimeIsLastEnd, + lastEventId: previousEvent.id, + after: previousEvent.id, }; addEvent(newEvent, options); - } else if (isPreviousDifferent && isNextDifferent) { - addEvent({ type, after: previousEvent.id }); + } else if (isPreviousDifferent && isNextDifferent && type !== 'clone') { + addEvent({ type }, { after: previousEvent.id }); } } }, @@ -67,33 +75,45 @@ export default function Rundown(props) { // Handle keyboard shortcuts const handleKeyPress = useCallback( - (event) => { + (event: KeyboardEvent) => { // handle held key if (event.repeat) return; // Check if the alt key is pressed if (event.altKey && (!event.ctrlKey || !event.shiftKey)) { - // Arrow down - if (event.keyCode === 40) { - if (cursor < entries.length - 1) moveCursorDown(); - } - // Arrow up - if (event.keyCode === 38) { - if (cursor > 0) moveCursorUp(); - } - if (event.code === 'KeyE') { - event.preventDefault(); - if (cursor == null) return; - insertAtCursor('event', cursor); - } - if (event.code === 'KeyD') { - event.preventDefault(); - if (cursor == null) return; - insertAtCursor('delay', cursor); - } - if (event.code === 'KeyB') { - event.preventDefault(); - if (cursor == null) return; - insertAtCursor('block', cursor); + + switch (event.code) { + case 'ArrowDown': { + if (cursor < entries.length - 1) moveCursorDown(); + break; + } + case 'ArrowUp': { + if (cursor > 0) moveCursorUp(); + break; + } + case 'KeyE': { + event.preventDefault(); + if (cursor === -1) return; + insertAtCursor(SupportedEvent.Event, cursor); + break; + } + case 'KeyD': { + event.preventDefault(); + if (cursor < 0) return; + insertAtCursor(SupportedEvent.Delay, cursor); + break; + } + case 'KeyB': { + event.preventDefault(); + if (cursor < 0) return; + insertAtCursor(SupportedEvent.Block, cursor); + break; + } + case 'KeyC': { + event.preventDefault(); + if (cursor < 0) return; + insertAtCursor('clone', cursor); + break; + } } } }, @@ -127,7 +147,9 @@ export default function Rundown(props) { // or cursor settings changed useEffect(() => { // and if we are locked - if (!isCursorLocked || data.selectedEventId == null) return; + if (!isCursorLocked || !data?.selectedEventId) { + return; + } // move cursor let gotoIndex = -1; @@ -143,11 +165,10 @@ export default function Rundown(props) { // move cursor moveCursorTo(gotoIndex); } - }, [data.selectedEventId, entries, isCursorLocked, moveCursorTo]); + }, [data?.selectedEventId, entries, isCursorLocked, moveCursorTo]); - // DND - const handleOnDragEnd = useCallback( - (result) => { + // @ts-expect-error react-beautiful-dnd stuff, cant type + const handleOnDragEnd = useCallback((result) => { // drop outside of area if (!result.destination) return; @@ -165,7 +186,7 @@ export default function Rundown(props) {