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
This commit is contained in:
Carlos Valente
2022-12-26 09:40:33 +01:00
committed by GitHub
parent 1bb67eb82a
commit 6f634c36f9
30 changed files with 354 additions and 180 deletions
+8 -2
View File
@@ -25,7 +25,7 @@ export async function requestPostEvent(data: OntimeRundownEntry) {
* @description HTTP request to put new event * @description HTTP request to put new event
* @return {Promise} * @return {Promise}
*/ */
export async function requestPutEvent(data: OntimeRundownEntry) { export async function requestPutEvent(data: Partial<OntimeRundownEntry>) {
return axios.put(rundownURL, data); return axios.put(rundownURL, data);
} }
@@ -37,11 +37,17 @@ export async function requestPatchEvent(data: OntimeRundownEntry) {
return axios.patch(rundownURL, data); return axios.patch(rundownURL, data);
} }
export type ReorderEntry = {
eventId: string,
from: number,
to: number,
}
/** /**
* @description HTTP request to reorder events * @description HTTP request to reorder events
* @return {Promise} * @return {Promise}
*/ */
export async function requestReorderEvent(data: OntimeRundownEntry) { export async function requestReorderEvent(data: ReorderEntry) {
return axios.patch(`${rundownURL}/reorder`, data); return axios.patch(`${rundownURL}/reorder`, data);
} }
@@ -1,14 +1,17 @@
import { Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
import style from './ColourInput.module.scss'; import style from './ColourInput.module.scss';
interface ColourInputProps { interface ColourInputProps {
value: string; value: string;
handleChange: (newValue: string) => void; name: EventEditorSubmitActions;
handleChange: (newValue: EventEditorSubmitActions, name: string) => void;
} }
export default function ColourInput(props: ColourInputProps) { export default function ColourInput(props: ColourInputProps) {
const { value, handleChange } = props; const { value, name, handleChange } = props;
return ( return (
<Input <Input
size='sm' size='sm'
@@ -16,7 +19,7 @@ export default function ColourInput(props: ColourInputProps) {
className={style.colourInput} className={style.colourInput}
type='color' type='color'
value={value} value={value}
onChange={(event) => handleChange(event.target.value)} onChange={(event) => handleChange(name, event.target.value)}
/> />
); );
} }
@@ -1,9 +1,11 @@
import { CSSProperties } from 'react';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg'; import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
import style from './Empty.module.scss'; import style from './Empty.module.scss';
interface EmptyProps { interface EmptyProps {
text: string; text: string;
style: CSSProperties;
} }
export default function Empty(props: EmptyProps) { export default function Empty(props: EmptyProps) {
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { ALIASES } from '../api/apiConstants'; import { ALIASES } from '../api/apiConstants';
import { getAliases } from '../api/ontimeApi'; import { getAliases } from '../api/ontimeApi';
@@ -15,6 +16,7 @@ export default function useAliases() {
placeholderData: [], placeholderData: [],
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { EVENT_TABLE } from '../api/apiConstants'; import { EVENT_TABLE } from '../api/apiConstants';
import { fetchEvent } from '../api/eventApi'; import { fetchEvent } from '../api/eventApi';
import { eventDataPlaceholder } from '../models/EventData.type'; import { eventDataPlaceholder } from '../models/EventData.type';
@@ -16,6 +17,7 @@ export default function useEvent() {
placeholderData: eventDataPlaceholder, placeholderData: eventDataPlaceholder,
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
+2
View File
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { APP_INFO } from '../api/apiConstants'; import { APP_INFO } from '../api/apiConstants';
import { getInfo } from '../api/ontimeApi'; import { getInfo } from '../api/ontimeApi';
import { ontimePlaceholderInfo } from '../models/Info.types'; import { ontimePlaceholderInfo } from '../models/Info.types';
@@ -16,6 +17,7 @@ export default function useInfo() {
placeholderData: ontimePlaceholderInfo, placeholderData: ontimePlaceholderInfo,
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { OSC_SETTINGS } from '../api/apiConstants'; import { OSC_SETTINGS } from '../api/apiConstants';
import { getOSC } from '../api/ontimeApi'; import { getOSC } from '../api/ontimeApi';
import { oscPlaceholderSettings } from '../models/OscSettings.type'; import { oscPlaceholderSettings } from '../models/OscSettings.type';
@@ -16,6 +17,7 @@ export default function useOscSettings() {
placeholderData: oscPlaceholderSettings, placeholderData: oscPlaceholderSettings,
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchInterval } from '../../ontimeConfig';
import { RUNDOWN_TABLE } from '../api/apiConstants'; import { RUNDOWN_TABLE } from '../api/apiConstants';
import { fetchRundown } from '../api/eventsApi'; import { fetchRundown } from '../api/eventsApi';
@@ -15,6 +16,7 @@ export default function useRundown() {
placeholderData: [], placeholderData: [],
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchInterval,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { APP_SETTINGS } from '../api/apiConstants'; import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi'; import { getSettings } from '../api/ontimeApi';
import { ontimePlaceholderSettings } from '../models/OntimeSettings.type'; import { ontimePlaceholderSettings } from '../models/OntimeSettings.type';
@@ -16,6 +17,7 @@ export default function useSettings() {
placeholderData: ontimePlaceholderSettings, placeholderData: ontimePlaceholderSettings,
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchInterval } from '../../ontimeConfig';
import { USERFIELDS } from '../api/apiConstants'; import { USERFIELDS } from '../api/apiConstants';
import { getUserFields } from '../api/ontimeApi'; import { getUserFields } from '../api/ontimeApi';
import { userFieldsPlaceholder } from '../models/UserFields.type'; import { userFieldsPlaceholder } from '../models/UserFields.type';
@@ -16,6 +17,7 @@ export default function useUserFields() {
placeholderData: userFieldsPlaceholder, placeholderData: userFieldsPlaceholder,
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchInterval,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { VIEW_SETTINGS } from '../api/apiConstants'; import { VIEW_SETTINGS } from '../api/apiConstants';
import { getView } from '../api/ontimeApi'; import { getView } from '../api/ontimeApi';
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
@@ -16,6 +17,7 @@ export default function useViewSettings() {
placeholderData: viewsSettingsPlaceholder, placeholderData: viewsSettingsPlaceholder,
retry: 5, retry: 5,
retryDelay: attempt => attempt * 2500, retryDelay: attempt => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -1,8 +1,11 @@
import { useCallback, useContext } from 'react'; import { useCallback, useContext } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; 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 { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
import { import {
ReorderEntry,
requestApplyDelay, requestApplyDelay,
requestDelete, requestDelete,
requestDeleteAll, requestDeleteAll,
@@ -10,7 +13,9 @@ import {
requestPutEvent, requestPutEvent,
requestReorderEvent, requestReorderEvent,
} from '../api/eventsApi'; } from '../api/eventsApi';
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings';
import { LoggingContext } from '../context/LoggingContext'; import { LoggingContext } from '../context/LoggingContext';
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from '../models/EventTypes';
/** /**
* @description Set of utilities for events * @description Set of utilities for events
@@ -18,9 +23,11 @@ import { LoggingContext } from '../context/LoggingContext';
export const useEventAction = () => { export const useEventAction = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext); 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 * @private
*/ */
const _addEventMutation = useMutation(requestPostEvent, { 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 * Adds an event to rundown
* @param {object} event - Event to be added
* @param {object} [options] - Event options
*/ */
const addEvent = useCallback( const addEvent = useCallback(
async (event, options) => { async (event: Partial<OntimeRundownEntry>, options?: AddOptions) => {
const newEvent = { ...event }; const newEvent: Partial<OntimeRundownEntry> = { ...event };
// ************* CHECK OPTIONS // ************* CHECK OPTIONS
// there is an option to pass an index of an array to use as start time // there is an option to pass an index of an array to use as start time
if (typeof options?.startIsLastEnd !== 'undefined') { // only events have options
const events = queryClient.getQueryData(RUNDOWN_TABLE); if (newEvent.type === SupportedEvent.Event) {
const previousEvent = events.find((event) => event.id === options.startIsLastEnd); const applicationOptions = {
newEvent.timeStart = previousEvent.timeEnd || 0; defaultPublic: options?.defaultPublic ?? defaultPublic,
} startTimeIsLastEnd: options?.startTimeIsLastEnd ?? startTimeIsLastEnd,
lastEventId: options?.lastEventId,
after: options?.after,
};
// hard coding duration value to be as expected for now // hard coding duration value to be as expected for now
// this until timeOptions gets implemented // this until timeOptions gets implemented
if (newEvent.type === 'event') { if (typeof newEvent?.timeStart !== 'undefined' && typeof newEvent.timeEnd !== 'undefined') {
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0; 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 { try {
// @ts-expect-error we know that the event here is one of the defined types
await _addEventMutation.mutateAsync(newEvent); await _addEventMutation.mutateAsync(newEvent);
} catch (error) { } 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 * @private
*/ */
const _updateEventMutation = useMutation(requestPutEvent, { const _updateEventMutation = useMutation(requestPutEvent, {
@@ -84,33 +123,37 @@ export const useEventAction = () => {
}, },
// Mutation fails, rollback undoes optimist update // Mutation fails, rollback undoes optimist update
onError: (error, newEvent, context) => { onError: (_error, _newEvent, context) => {
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context.newEvent.id], context.previousEvent); queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: async (newEvent) => { onSettled: async () => {
await queryClient.invalidateQueries([RUNDOWN_TABLE_KEY, newEvent.id]); await queryClient.invalidateQueries([RUNDOWN_TABLE_KEY]);
}, },
}); });
/** /**
* @description Updates existing event * Updates existing event
* @param {object} event - Event to be added
*/ */
const updateEvent = useCallback( const updateEvent = useCallback(
async (event) => { async (event: Partial<OntimeRundownEntry>) => {
try { try {
await _updateEventMutation.mutateAsync(event); await _updateEventMutation.mutateAsync(event);
} catch (error) { } 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], [_updateEventMutation, emitError],
); );
/** /**
* @description Calls mutation to delete an event * Calls mutation to delete an event
* @private * @private
*/ */
const _deleteEventMutation = useMutation(requestDelete, { const _deleteEventMutation = useMutation(requestDelete, {
@@ -122,7 +165,7 @@ export const useEventAction = () => {
// Snapshot the previous value // Snapshot the previous value
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE); 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 // optimistically update object
queryClient.setQueryData(RUNDOWN_TABLE, filtered); queryClient.setQueryData(RUNDOWN_TABLE, filtered);
@@ -132,8 +175,8 @@ export const useEventAction = () => {
}, },
// Mutation fails, rollback undoes optimist update // Mutation fails, rollback undoes optimist update
onError: (error, eventId, context) => { onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context.previousEvents); queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
@@ -143,22 +186,25 @@ export const useEventAction = () => {
}); });
/** /**
* @description Deletes an event form the list * Deletes an event form the list
* @param {object} eventId - Event to be deleted
*/ */
const deleteEvent = useCallback( const deleteEvent = useCallback(
async (eventId) => { async (eventId: string) => {
try { try {
await _deleteEventMutation.mutateAsync(eventId); await _deleteEventMutation.mutateAsync(eventId);
} catch (error) { } catch (error) {
emitError(`Error deleting event: ${error.message}`); if(!axios.isAxiosError(error)){
emitError(`Error deleting event: ${(error as AxiosError).message}`);
} else {
emitError(`Error deleting event: ${error}`);
}
} }
}, },
[_deleteEventMutation, emitError], [_deleteEventMutation, emitError],
); );
/** /**
* @description Calls mutation to delete all events * Calls mutation to delete all events
* @private * @private
*/ */
const _deleteAllEventsMutation = useMutation(requestDeleteAll, { const _deleteAllEventsMutation = useMutation(requestDeleteAll, {
@@ -170,18 +216,16 @@ export const useEventAction = () => {
// Snapshot the previous value // Snapshot the previous value
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE); const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
const clear = [];
// optimistically update object // optimistically update object
queryClient.setQueryData(RUNDOWN_TABLE, clear); queryClient.setQueryData(RUNDOWN_TABLE, []);
// Return a context with the previous and new events // Return a context with the previous and new events
return { previousEvents }; return { previousEvents };
}, },
// Mutation fails, rollback undos optimist update // Mutation fails, rollback undos optimist update
onError: (error, eventId, context) => { onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context.previousEvents); queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // 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 () => { const deleteAllEvents = useCallback(async () => {
try { try {
await _deleteAllEventsMutation.mutateAsync(); await _deleteAllEventsMutation.mutateAsync();
} catch (error) { } 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]); }, [_deleteAllEventsMutation, emitError]);
/** /**
* @description Calls mutation to apply a delay * Calls mutation to apply a delay
* @private * @private
*/ */
const _applyDelayMutation = useMutation(requestApplyDelay, { const _applyDelayMutation = useMutation(requestApplyDelay, {
@@ -213,22 +261,25 @@ export const useEventAction = () => {
}); });
/** /**
* @description Applies a given delay * Applies a given delay block
* @param {object} delayEventId - Id of delay to be applied
*/ */
const applyDelay = useCallback( const applyDelay = useCallback(
async (delayEventId) => { async (delayEventId: string) => {
try { try {
await _applyDelayMutation.mutateAsync(delayEventId); await _applyDelayMutation.mutateAsync(delayEventId);
} catch (error) { } 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], [_applyDelayMutation, emitError],
); );
/** /**
* @description Calls mutation to reorder an event * Calls mutation to reorder an event
* @private * @private
*/ */
const _reorderEventMutation = useMutation(requestReorderEvent, { const _reorderEventMutation = useMutation(requestReorderEvent, {
@@ -240,7 +291,7 @@ export const useEventAction = () => {
// Snapshot the previous value // Snapshot the previous value
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE); const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
const e = [...previousEvents]; const e = [...(previousEvents as OntimeRundown)];
const [reorderedItem] = e.splice(data.from, 1); const [reorderedItem] = e.splice(data.from, 1);
e.splice(data.to, 0, reorderedItem); e.splice(data.to, 0, reorderedItem);
@@ -252,8 +303,8 @@ export const useEventAction = () => {
}, },
// Mutation fails, rollback undoes optimist update // Mutation fails, rollback undoes optimist update
onError: (error, eventId, context) => { onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context.previousEvents); queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
@@ -263,22 +314,23 @@ export const useEventAction = () => {
}); });
/** /**
* @description Reorders a given event * Reorders a given event
* @param {string} eventID - ID of event to reorder
* @param {number} from - Current index
* @param {number} to - New Index
*/ */
const reorderEvent = useCallback( const reorderEvent = useCallback(
async (eventId, from, to) => { async (eventId: string, from: number, to: number) => {
try { try {
const reorderObject = { const reorderObject: ReorderEntry = {
eventId: eventId, eventId: eventId,
from: from, from: from,
to: to, to: to,
}; };
await _reorderEventMutation.mutateAsync(reorderObject); await _reorderEventMutation.mutateAsync(reorderObject);
} catch (error) { } 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], [_reorderEventMutation, emitError],
+16 -4
View File
@@ -14,17 +14,29 @@ export default function useFullscreen() {
return () => { return () => {
document.removeEventListener('fullscreenchange', handleChange, { passive: true }); document.removeEventListener('fullscreenchange', handleChange, { passive: true });
document.removeEventListener('resize', handleChange, { passive: true }); document.removeEventListener('resize', handleChange, { passive: true });
}; }, []); };
}, []);
const toggleFullScreen = useCallback(() => { const toggleFullScreen = useCallback(() => {
if (!document.fullscreenElement) { if (!document.fullscreenElement && !document.webkitIsFullScreen) {
document.documentElement.requestFullscreen(); // 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 { } else {
// Fullscreen mode is active, so we can exit fullscreen mode
if (document.exitFullscreen) { if (document.exitFullscreen) {
// Standard fullscreen API is supported
document.exitFullscreen(); document.exitFullscreen();
} else if (document.webkitCancelFullscreen) {
// iOS Safari fullscreen API is supported
document.webkitCancelFullscreen();
} }
} }
setFullScreen(document.fullscreenElement);
}, []); }, []);
return { isFullScreen, toggleFullScreen }; return { isFullScreen, toggleFullScreen };
+9 -2
View File
@@ -10,6 +10,7 @@ import {
FEAT_RUNDOWN, FEAT_RUNDOWN,
TIMER, TIMER,
} from '../api/apiConstants'; } from '../api/apiConstants';
import { Playstate } from '../models/OntimeTypes';
function createSocketHook<T>(key: string, defaultValue: T | null = null) { function createSocketHook<T>(key: string, defaultValue: T | null = null) {
subscribeOnce<T>(key, (data) => queryClient.setQueryData([key], data)); subscribeOnce<T>(key, (data) => queryClient.setQueryData([key], data));
@@ -21,8 +22,14 @@ function createSocketHook<T>(key: string, defaultValue: T | null = null) {
return () => useQuery({ queryKey: [key], queryFn: fetcher, placeholderData: defaultValue }); return () => useQuery({ queryKey: [key], queryFn: fetcher, placeholderData: defaultValue });
} }
const emptyRundown = { interface IRundown {
selectEventId: null, selectedEventId: string | null;
nextEventId: string | null;
playback: Playstate | null;
}
const emptyRundown: IRundown = {
selectedEventId: null,
nextEventId: null, nextEventId: null,
playback: null, playback: null,
}; };
+10 -5
View File
@@ -1,22 +1,27 @@
export type EventTypes = 'event' | 'delay' | 'block'; export enum SupportedEvent {
Event = 'event',
Delay = 'delay',
Block = 'block'
}
export interface OntimeBaseEvent { export interface OntimeBaseEvent {
type: EventTypes; type: SupportedEvent;
id: string; id: string;
after?: string; // used when creating an event to indicate its position in rundown
} }
export type OntimeDelay = OntimeBaseEvent & { export type OntimeDelay = OntimeBaseEvent & {
type: 'delay'; type: SupportedEvent.Delay;
duration: number; duration: number;
revision: number; revision: number;
} }
export type OntimeBlock = OntimeBaseEvent & { export type OntimeBlock = OntimeBaseEvent & {
type: 'block'; type: SupportedEvent.Block;
} }
export type OntimeEvent = OntimeBaseEvent & { export type OntimeEvent = OntimeBaseEvent & {
type: 'event'; type: SupportedEvent.Event;
title: string, title: string,
subtitle: string, subtitle: string,
presenter: string, presenter: string,
@@ -43,7 +43,7 @@ export default function Transport(props: TransportProps) {
<Tooltip label='Reload event' openDelay={tooltipDelayMid}> <Tooltip label='Reload event' openDelay={tooltipDelayMid}>
<TapButton <TapButton
onClick={() => setPlayback.reload()} onClick={() => setPlayback.reload()}
disabled={selectedId == null || isRolling || noEvents} disabled={!selectedId || noEvents}
> >
<IoReload className={style.invertX} /> <IoReload className={style.invertX} />
</TapButton> </TapButton>
@@ -51,7 +51,7 @@ export default function Transport(props: TransportProps) {
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}> <Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton <TapButton
onClick={() => setPlayback.stop()} onClick={() => setPlayback.stop()}
disabled={(selectedId == null && !isRolling) || noEvents} disabled={!selectedId}
theme='stop' theme='stop'
> >
<IoStop /> <IoStop />
@@ -117,7 +117,7 @@ export default function EventEditor() {
return ( return (
<div className={style.eventEditor}> <div className={style.eventEditor}>
<div className={style.eventInfo}>{`Event ${'not yet'} | Event ID ${event.id}`}</div> <div className={style.eventInfo}>{`Event ID ${event.id}`}</div>
<div className={style.eventActions}> <div className={style.eventActions}>
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag> <CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
</div> </div>
@@ -207,7 +207,8 @@ export default function EventEditor() {
<div className={style.inline}> <div className={style.inline}>
<ColourInput <ColourInput
value={event?.colour} value={event?.colour}
handleChange={(value) => handleSubmit('colour', value)} name='colour'
handleChange={handleSubmit}
/> />
<Button <Button
leftIcon={<IoBan />} leftIcon={<IoBan />}
@@ -1,4 +1,5 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import isEqual from 'react-fast-compare';
import { import {
Checkbox, Checkbox,
FormControl, FormControl,
@@ -56,11 +57,13 @@ export default function AppSettingsModal() {
const submitHandler = async (event) => { const submitHandler = async (event) => {
event.preventDefault(); event.preventDefault();
setSubmitting(true); setSubmitting(true);
const validation = { isValid: false, message: '' };
// set context const hasChanged = !isEqual(formSettings,eventSettings);
// TODO: add fast-equals here and check if event settings have changed if (hasChanged) {
saveEventSettings(formSettings); saveEventSettings(formSettings);
const validation = { isValid: false }; validation.isValid = true;
}
// we might not have changed this // we might not have changed this
if (formData.pinCode !== data.pinCode) { if (formData.pinCode !== data.pinCode) {
@@ -77,9 +80,12 @@ export default function AppSettingsModal() {
if (formData.timeFormat !== data.timeFormat) { if (formData.timeFormat !== data.timeFormat) {
if (formData.timeFormat === '12' || formData.timeFormat === '24') { if (formData.timeFormat === '12' || formData.timeFormat === '24') {
validation.isValid = true; validation.isValid = true;
} else {
validation.isValue = false;
} }
} }
let resetChange = hasChanged;
// set fields with error // set fields with error
if (!validation.isValid) { if (!validation.isValid) {
emitError(`Invalid Input: ${validation.message}`); emitError(`Invalid Input: ${validation.message}`);
@@ -90,10 +96,13 @@ export default function AppSettingsModal() {
emitError(`Error saving settings: ${error}`) emitError(`Error saving settings: ${error}`)
} finally { } finally {
await refetch(); await refetch();
setChanged(false); resetChange = true;
} }
validation?.message && emitWarning(validation.message); validation?.message && emitWarning(validation.message);
} }
if (resetChange) {
setChanged(false);
}
setSubmitting(false); setSubmitting(false);
}; };
@@ -189,8 +198,7 @@ export default function AppSettingsModal() {
colorScheme='red' colorScheme='red'
variant='ghost' variant='ghost'
icon={<FiX />} icon={<FiX />}
onMouseDown={() => handleChange('pinCode', '')} clickHandler={() => handleChange('pinCode', '')}
onMouseUp={() => handleChange('pinCode', '')}
isDisabled={disableModal} isDisabled={disableModal}
/> />
</div> </div>
@@ -6,7 +6,7 @@ import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInforma
import { postView } from '../../common/api/ontimeApi'; import { postView } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn'; import EnableBtn from '../../common/components/buttons/EnableBtn';
import { LoggingContext } from '../../common/context/LoggingContext'; 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 { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
import { openLink } from '../../common/utils/linkUtils'; import { openLink } from '../../common/utils/linkUtils';
@@ -15,7 +15,8 @@ import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss'; import style from './Modals.module.scss';
export default function ViewsSettingsModal() { export default function ViewsSettingsModal() {
const { data, status, refetch } = useSettings(); const { data, status, refetch } = useViewSettings();
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(viewsSettingsPlaceholder); const [formData, setFormData] = useState(viewsSettingsPlaceholder);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
@@ -11,6 +11,7 @@ import Empty from 'common/components/state/Empty';
import { CursorContext } from 'common/context/CursorContext'; import { CursorContext } from 'common/context/CursorContext';
import { useEventAction } from 'common/hooks/useEventAction'; import { useEventAction } from 'common/hooks/useEventAction';
import { useRundownEditor } from 'common/hooks/useSocket'; import { useRundownEditor } from 'common/hooks/useSocket';
import { OntimeRundown, SupportedEvent } from 'common/models/EventTypes';
import { cloneEvent } from 'common/utils/eventsManager'; import { cloneEvent } from 'common/utils/eventsManager';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -20,21 +21,27 @@ import RundownEntry from './RundownEntry';
import style from './Rundown.module.scss'; import style from './Rundown.module.scss';
export default function Rundown(props) { interface RundownProps {
entries: OntimeRundown;
}
export default function Rundown(props: RundownProps) {
const { entries } = props; const { entries } = props;
// Todo: add selectedId and nextId to rundown editor hook
const { data } = useRundownEditor(); const { data } = useRundownEditor();
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } =
useContext(CursorContext); useContext(CursorContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom); const defaultPublic = useAtomValue(defaultPublicAtom);
const { addEvent, reorderEvent } = useEventAction(); const { addEvent, reorderEvent } = useEventAction();
const cursorRef = createRef(); const cursorRef = createRef<HTMLDivElement>();
const showQuickEntry = useAtomValue(showQuickEntryAtom); const showQuickEntry = useAtomValue(showQuickEntryAtom);
const insertAtCursor = useCallback( const insertAtCursor = useCallback(
(type, cursor) => { (type: SupportedEvent | 'clone', cursor: number) => {
if (cursor === -1) { if (cursor === -1) {
if (type === 'clone') {
return;
}
addEvent({ type }); addEvent({ type });
} else { } else {
const previousEvent = entries?.[cursor]; const previousEvent = entries?.[cursor];
@@ -43,22 +50,23 @@ export default function Rundown(props) {
// prevent adding two non-event blocks consecutively // prevent adding two non-event blocks consecutively
const isPreviousDifferent = previousEvent?.type !== type; const isPreviousDifferent = previousEvent?.type !== type;
const isNextDifferent = nextEvent?.type !== type; const isNextDifferent = nextEvent?.type !== type;
if (type === 'clone' && previousEvent) { if (type === 'clone' && previousEvent?.type === SupportedEvent.Event) {
const newEvent = cloneEvent(previousEvent); const newEvent = cloneEvent(previousEvent);
newEvent.after = previousEvent.id; newEvent.after = previousEvent.id;
addEvent(newEvent); addEvent(newEvent);
} else if (type === 'event') { } else if (type === SupportedEvent.Event) {
const newEvent = { const newEvent = {
type: 'event', type: SupportedEvent.Event,
after: previousEvent.id,
isPublic: defaultPublic,
}; };
const options = { const options = {
startIsLastEnd: startTimeIsLastEnd ? previousEvent.id : undefined, defaultPublic: defaultPublic,
startTimeIsLastEnd: startTimeIsLastEnd,
lastEventId: previousEvent.id,
after: previousEvent.id,
}; };
addEvent(newEvent, options); addEvent(newEvent, options);
} else if (isPreviousDifferent && isNextDifferent) { } else if (isPreviousDifferent && isNextDifferent && type !== 'clone') {
addEvent({ type, after: previousEvent.id }); addEvent({ type }, { after: previousEvent.id });
} }
} }
}, },
@@ -67,33 +75,45 @@ export default function Rundown(props) {
// Handle keyboard shortcuts // Handle keyboard shortcuts
const handleKeyPress = useCallback( const handleKeyPress = useCallback(
(event) => { (event: KeyboardEvent) => {
// handle held key // handle held key
if (event.repeat) return; if (event.repeat) return;
// Check if the alt key is pressed // Check if the alt key is pressed
if (event.altKey && (!event.ctrlKey || !event.shiftKey)) { if (event.altKey && (!event.ctrlKey || !event.shiftKey)) {
// Arrow down
if (event.keyCode === 40) { switch (event.code) {
if (cursor < entries.length - 1) moveCursorDown(); case 'ArrowDown': {
} if (cursor < entries.length - 1) moveCursorDown();
// Arrow up break;
if (event.keyCode === 38) { }
if (cursor > 0) moveCursorUp(); case 'ArrowUp': {
} if (cursor > 0) moveCursorUp();
if (event.code === 'KeyE') { break;
event.preventDefault(); }
if (cursor == null) return; case 'KeyE': {
insertAtCursor('event', cursor); event.preventDefault();
} if (cursor === -1) return;
if (event.code === 'KeyD') { insertAtCursor(SupportedEvent.Event, cursor);
event.preventDefault(); break;
if (cursor == null) return; }
insertAtCursor('delay', cursor); case 'KeyD': {
} event.preventDefault();
if (event.code === 'KeyB') { if (cursor < 0) return;
event.preventDefault(); insertAtCursor(SupportedEvent.Delay, cursor);
if (cursor == null) return; break;
insertAtCursor('block', cursor); }
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 // or cursor settings changed
useEffect(() => { useEffect(() => {
// and if we are locked // and if we are locked
if (!isCursorLocked || data.selectedEventId == null) return; if (!isCursorLocked || !data?.selectedEventId) {
return;
}
// move cursor // move cursor
let gotoIndex = -1; let gotoIndex = -1;
@@ -143,11 +165,10 @@ export default function Rundown(props) {
// move cursor // move cursor
moveCursorTo(gotoIndex); moveCursorTo(gotoIndex);
} }
}, [data.selectedEventId, entries, isCursorLocked, moveCursorTo]); }, [data?.selectedEventId, entries, isCursorLocked, moveCursorTo]);
// DND // @ts-expect-error react-beautiful-dnd stuff, cant type
const handleOnDragEnd = useCallback( const handleOnDragEnd = useCallback((result) => {
(result) => {
// drop outside of area // drop outside of area
if (!result.destination) return; if (!result.destination) return;
@@ -165,7 +186,7 @@ export default function Rundown(props) {
<div className={style.alignCenter}> <div className={style.alignCenter}>
<Empty text='No data yet' style={{ marginTop: '7vh' }} /> <Empty text='No data yet' style={{ marginTop: '7vh' }} />
<Button <Button
onClick={() => insertAtCursor('event', cursor)} onClick={() => insertAtCursor(SupportedEvent.Event, -1)}
variant='ontime-filled' variant='ontime-filled'
className={style.spaceTop} className={style.spaceTop}
leftIcon={<IoAdd />} leftIcon={<IoAdd />}
@@ -179,7 +200,7 @@ export default function Rundown(props) {
let eventIndex = -1; let eventIndex = -1;
let previousEnd = 0; let previousEnd = 0;
let thisEnd = 0; let thisEnd = 0;
let previousEventId = null; let previousEventId: string | undefined;
return ( return (
<div className={style.eventContainer}> <div className={style.eventContainer}>
@@ -203,6 +224,9 @@ export default function Rundown(props) {
previousEventId = entry.id; previousEventId = entry.id;
} }
const isLast = index === entries.length - 1; const isLast = index === entries.length - 1;
const isSelected = data?.selectedEventId === entry.id;
const isNext = data?.nextEventId === entry.id;
return ( return (
<Fragment key={entry.id}> <Fragment key={entry.id}>
<div ref={cursor === index ? cursorRef : undefined}> <div ref={cursor === index ? cursorRef : undefined}>
@@ -211,18 +235,19 @@ export default function Rundown(props) {
index={index} index={index}
eventIndex={eventIndex} eventIndex={eventIndex}
data={entry} data={entry}
selected={data.selectedEventId === entry.id} selected={isSelected}
hasCursor={cursor === index} hasCursor={cursor === index}
next={data.nextEventId === entry.id} next={isNext}
delay={cumulativeDelay} delay={cumulativeDelay}
previousEnd={previousEnd} previousEnd={previousEnd}
playback={data.selectedEventId === entry.id ? data.playback : undefined} previousEventId={previousEventId}
playback={isSelected ? data.playback || undefined : undefined}
/> />
</div> </div>
{((showQuickEntry && index === cursor) || isLast) && ( {((showQuickEntry && index === cursor) || isLast) && (
<QuickAddBlock <QuickAddBlock
showKbd={index === cursor} showKbd={index === cursor}
previousId={entry.id} eventId={entry.id}
previousEventId={previousEventId} previousEventId={previousEventId}
disableAddDelay={entry.type === 'delay'} disableAddDelay={entry.type === 'delay'}
disableAddBlock={entry.type === 'block'} disableAddBlock={entry.type === 'block'}
+20 -18
View File
@@ -6,7 +6,7 @@ import {
} from 'common/atoms/LocalEventSettings'; } from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext'; import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction'; import { useEventAction } from 'common/hooks/useEventAction';
import { OntimeEvent, OntimeRundownEntry } from 'common/models/EventTypes'; import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'common/models/EventTypes';
import { Playstate } from 'common/models/OntimeTypes'; import { Playstate } from 'common/models/OntimeTypes';
import { cloneEvent } from 'common/utils/eventsManager'; import { cloneEvent } from 'common/utils/eventsManager';
import { calculateDuration } from 'common/utils/timesManager'; import { calculateDuration } from 'common/utils/timesManager';
@@ -28,6 +28,7 @@ export type EventItemActions =
| 'update' | 'update'
interface RundownEntryProps { interface RundownEntryProps {
type: SupportedEvent;
index: number; index: number;
eventIndex: number; eventIndex: number;
data: OntimeRundownEntry; data: OntimeRundownEntry;
@@ -36,7 +37,8 @@ interface RundownEntryProps {
next: boolean; next: boolean;
delay: number; delay: number;
previousEnd: number; previousEnd: number;
playback: Playstate; previousEventId?: string;
playback?: Playstate; // we only care about this if this event is playing
} }
export default function RundownEntry(props: RundownEntryProps) { export default function RundownEntry(props: RundownEntryProps) {
@@ -49,6 +51,7 @@ export default function RundownEntry(props: RundownEntryProps) {
next, next,
delay, delay,
previousEnd, previousEnd,
previousEventId,
playback, playback,
} = props; } = props;
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
@@ -71,30 +74,29 @@ export default function RundownEntry(props: RundownEntryProps) {
break; break;
} }
case 'event': { case 'event': {
const newEvent = { const newEvent = { type: SupportedEvent.Event };
type: 'event',
after: data.id,
isPublic: defaultPublic,
};
const options = { const options = {
startIsLastEnd: startTimeIsLastEnd ? data.id : undefined, startTimeIsLastEnd,
defaultPublic,
lastEventId: previousEventId,
after: data.id,
}; };
addEvent(newEvent, options); addEvent(newEvent, options);
break; break;
} }
case 'delay': { case 'delay': {
addEvent({ type: 'delay', after: data.id }); addEvent({ type: SupportedEvent.Delay }, { after: data.id });
break; break;
} }
case 'block': { case 'block': {
addEvent({ type: 'block', after: data.id }); addEvent({ type: SupportedEvent.Block }, { after: data.id });
break; break;
} }
case 'delete': { case 'delete': {
deleteEvent(data.id);
if (openId === data.id) { if (openId === data.id) {
setOpenId(null); setOpenId(null);
} }
deleteEvent(data.id);
break; break;
} }
case 'clone': { case 'clone': {
@@ -107,15 +109,15 @@ export default function RundownEntry(props: RundownEntryProps) {
const { field, value } = payload as FieldValue; const { field, value } = payload as FieldValue;
const newData: Partial<OntimeEvent> = { id: data.id }; const newData: Partial<OntimeEvent> = { id: data.id };
if (field === 'duration' && data.type === 'event') { if (field === 'duration' && data.type === SupportedEvent.Event) {
// duration defines timeEnd // duration defines timeEnd
newData.timeEnd = data.timeStart += value as number; newData.timeEnd = data.timeStart += value as number;
updateEvent(newData); updateEvent(newData);
} else if (field === 'timeStart' && data.type === 'event') { } else if (field === 'timeStart' && data.type === SupportedEvent.Event) {
newData.duration = calculateDuration(value as number, data.timeEnd); newData.duration = calculateDuration(value as number, data.timeEnd);
newData.timeStart = value as number; newData.timeStart = value as number;
updateEvent(newData); updateEvent(newData);
} else if (field === 'timeEnd' && data.type === 'event') { } else if (field === 'timeEnd' && data.type === SupportedEvent.Event) {
newData.duration = calculateDuration(data.timeStart, value as number); newData.duration = calculateDuration(data.timeStart, value as number);
newData.timeEnd = value as number; newData.timeEnd = value as number;
updateEvent(newData); updateEvent(newData);
@@ -132,10 +134,10 @@ export default function RundownEntry(props: RundownEntryProps) {
break; break;
} }
}, },
[addEvent, data, defaultPublic, deleteEvent, emitError, moveCursorTo, openId, setOpenId, startTimeIsLastEnd, updateEvent], [addEvent, data, defaultPublic, deleteEvent, emitError, moveCursorTo, openId, previousEventId, setOpenId, startTimeIsLastEnd, updateEvent],
); );
if (data.type === 'event') { if (data.type === SupportedEvent.Event) {
return ( return (
<EventBlock <EventBlock
timeStart={data.timeStart} timeStart={data.timeStart}
@@ -158,14 +160,14 @@ export default function RundownEntry(props: RundownEntryProps) {
actionHandler={actionHandler} actionHandler={actionHandler}
/> />
); );
} else if (data.type === 'block') { } else if (data.type === SupportedEvent.Block) {
return <BlockBlock return <BlockBlock
index={index} index={index}
data={data} data={data}
hasCursor={hasCursor} hasCursor={hasCursor}
actionHandler={actionHandler} actionHandler={actionHandler}
/>; />;
} else if (data.type === 'delay') { } else if (data.type === SupportedEvent.Delay) {
return <DelayBlock return <DelayBlock
index={index} index={index}
data={data} data={data}
@@ -89,6 +89,10 @@
grid-area: title; grid-area: title;
display: block; display: block;
font-size: 18px; font-size: 18px;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.noTitle { &.noTitle {
.preview { .preview {
@@ -150,8 +150,8 @@ export default function EventBlock(props: EventBlockProps) {
variant='ontime-subtle-white' variant='ontime-subtle-white'
aria-label='Skip event' aria-label='Skip event'
tooltip='Skip event' tooltip='Skip event'
openDelay={tooltipDelayMid}
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />} icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
{...tooltipProps}
{...blockBtnStyle} {...blockBtnStyle}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })} clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
tabIndex={-1} tabIndex={-1}
@@ -161,9 +161,9 @@ export default function EventBlock(props: EventBlockProps) {
variant='ontime-subtle-white' variant='ontime-subtle-white'
aria-label='Load event' aria-label='Load event'
tooltip='Load event' tooltip='Load event'
openDelay={tooltipDelayMid}
icon={<IoReload className={style.flip} />} icon={<IoReload className={style.flip} />}
disabled={skip} disabled={skip}
{...tooltipProps}
{...blockBtnStyle} {...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)} clickHandler={() => setEventPlayback.loadEvent(eventId)}
tabIndex={-1} tabIndex={-1}
@@ -172,9 +172,9 @@ export default function EventBlock(props: EventBlockProps) {
variant='ontime-subtle-white' variant='ontime-subtle-white'
aria-label='Start event' aria-label='Start event'
tooltip='Start event' tooltip='Start event'
openDelay={tooltipDelayMid}
icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />} icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />}
disabled={skip} disabled={skip}
{...tooltipProps}
{...blockBtnStyle} {...blockBtnStyle}
clickHandler={() => setEventPlayback.startEvent(eventId)} clickHandler={() => setEventPlayback.startEvent(eventId)}
backgroundColor={eventIsPlaying ? '#58A151' : undefined} backgroundColor={eventIsPlaying ? '#58A151' : undefined}
@@ -193,7 +193,7 @@ export default function EventBlock(props: EventBlockProps) {
<Editable <Editable
variant='ontime' variant='ontime'
value={blockTitle} value={blockTitle}
className={`${style.eventTitle} ${!title || title === '' ? style.noTitle : ''}`} className={`${style.eventTitle} ${!title ? style.noTitle : ''}`}
placeholder='Event title' placeholder='Event title'
onChange={(value) => setBlockTitle(value)} onChange={(value) => setBlockTitle(value)}
onSubmit={(value) => handleTitle(value)} onSubmit={(value) => handleTitle(value)}
@@ -246,7 +246,7 @@ export default function EventBlock(props: EventBlockProps) {
showDelay showDelay
showBlock showBlock
showClone showClone
enableDelete enableDelete={!selected}
actionHandler={actionHandler} actionHandler={actionHandler}
/> />
</div> </div>
@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import { import {
IconButton, IconButton,
Menu, Menu,
@@ -29,6 +30,12 @@ interface BlockActionMenuProps {
export default function BlockActionMenu(props: BlockActionMenuProps) { export default function BlockActionMenu(props: BlockActionMenuProps) {
const { showAdd, showDelay, showBlock, enableDelete, showClone, actionHandler, className } = props; const { showAdd, showDelay, showBlock, enableDelete, showClone, actionHandler, className } = props;
const handleAddEvent = useCallback(() => actionHandler("event"), [actionHandler])
const handleAddDelay = useCallback(() => actionHandler("delay"), [actionHandler])
const handleAddBlock = useCallback(() => actionHandler("block"), [actionHandler])
const handleClone = useCallback(() => actionHandler("clone"), [actionHandler])
const handleDelete = useCallback(() => actionHandler("delete"), [actionHandler])
return ( return (
<Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'> <Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<Tooltip label='Add ...' openDelay={tooltipDelayMid}> <Tooltip label='Add ...' openDelay={tooltipDelayMid}>
@@ -44,19 +51,19 @@ export default function BlockActionMenu(props: BlockActionMenuProps) {
/> />
</Tooltip> </Tooltip>
<MenuList> <MenuList>
<MenuItem icon={<IoAdd />} onClick={() => actionHandler('event')} isDisabled={!showAdd}> <MenuItem icon={<IoAdd />} onClick={handleAddEvent} isDisabled={!showAdd}>
Add Event after Add Event after
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon={<IoTimerOutline />} icon={<IoTimerOutline />}
onClick={() => actionHandler('delay')} onClick={handleAddDelay}
isDisabled={!showDelay} isDisabled={!showDelay}
> >
Add Delay after Add Delay after
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon={<IoRemoveCircleOutline />} icon={<IoRemoveCircleOutline />}
onClick={() => actionHandler('block')} onClick={handleAddBlock}
isDisabled={!showBlock} isDisabled={!showBlock}
> >
Add Block after Add Block after
@@ -64,7 +71,7 @@ export default function BlockActionMenu(props: BlockActionMenuProps) {
{showClone && ( {showClone && (
<MenuItem <MenuItem
icon={<IoDuplicateOutline />} icon={<IoDuplicateOutline />}
onClick={() => actionHandler('clone')} onClick={handleClone}
isDisabled={!showBlock} isDisabled={!showBlock}
> >
Clone event Clone event
@@ -73,7 +80,7 @@ export default function BlockActionMenu(props: BlockActionMenuProps) {
<MenuDivider /> <MenuDivider />
<MenuItem <MenuItem
icon={<IoTrashBinSharp />} icon={<IoTrashBinSharp />}
onClick={() => actionHandler('delete')} onClick={handleDelete}
isDisabled={!enableDelete} isDisabled={!enableDelete}
color='#D20300' color='#D20300'
> >
@@ -3,7 +3,7 @@ import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings'; import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext'; import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction'; import { useEventAction } from 'common/hooks/useEventAction';
import { EventTypes } from 'common/models/EventTypes'; import { SupportedEvent } from 'common/models/EventTypes';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { tooltipDelayMid } from '../../../ontimeConfig'; import { tooltipDelayMid } from '../../../ontimeConfig';
@@ -12,8 +12,8 @@ import style from './QuickAddBlock.module.scss';
interface QuickAddBlockProps { interface QuickAddBlockProps {
showKbd: boolean; showKbd: boolean;
previousId?: string; eventId: string;
previousEventId: string | null; previousEventId?: string;
disableAddDelay?: boolean; disableAddDelay?: boolean;
disableAddBlock: boolean; disableAddBlock: boolean;
} }
@@ -21,7 +21,7 @@ interface QuickAddBlockProps {
export default function QuickAddBlock(props: QuickAddBlockProps) { export default function QuickAddBlock(props: QuickAddBlockProps) {
const { const {
showKbd, showKbd,
previousId, eventId,
previousEventId, previousEventId,
disableAddDelay = true, disableAddDelay = true,
disableAddBlock, disableAddBlock,
@@ -33,23 +33,36 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
const doStartTime = useRef<HTMLInputElement | null>(null); const doStartTime = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null); const doPublic = useRef<HTMLInputElement | null>(null);
const handleCreateEvent = useCallback((eventType: EventTypes) => { const handleCreateEvent = useCallback((eventType: SupportedEvent) => {
switch (eventType) { switch (eventType) {
case 'event': { case 'event': {
const isPublicOption = doPublic?.current?.checked || defaultPublic; const isPublicOption = doPublic?.current?.checked;
const startTimeIsLastEndOption = doStartTime?.current?.checked || doStartTime; const startTimeIsLastEndOption = doStartTime?.current?.checked;
const newEvent = { type: 'event', after: previousId, isPublic: isPublicOption }; const newEvent = { type: SupportedEvent.Event };
const options = { startIsLastEnd: startTimeIsLastEndOption ? previousEventId : undefined }; const options = {
defaultPublic: isPublicOption,
startTimeIsLastEnd: startTimeIsLastEndOption,
lastEventId: previousEventId,
after: eventId,
};
addEvent(newEvent, options); addEvent(newEvent, options);
break; break;
} }
case 'delay': { case 'delay': {
addEvent({ type: 'delay', after: previousId }); const options = {
lastEventId: previousEventId,
after: eventId,
}
addEvent({ type: SupportedEvent.Delay }, options);
break; break;
} }
case 'block': { case 'block': {
addEvent({ type: 'block', after: previousId }); const options= {
lastEventId: previousEventId,
after: eventId,
}
addEvent({ type: SupportedEvent.Block }, options);
break; break;
} }
default: { default: {
@@ -58,14 +71,14 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
} }
} }
}, [defaultPublic, previousId, previousEventId, addEvent, emitError]); }, [previousEventId, eventId, addEvent, emitError]);
return ( return (
<div className={style.quickAdd}> <div className={style.quickAdd}>
<div className={style.btnRow}> <div className={style.btnRow}>
<Tooltip label='Add Event' openDelay={tooltipDelayMid}> <Tooltip label='Add Event' openDelay={tooltipDelayMid}>
<Button <Button
onClick={() => handleCreateEvent('event')} onClick={() => handleCreateEvent(SupportedEvent.Event)}
size='xs' size='xs'
variant='ontime-subtle-white' variant='ontime-subtle-white'
className={style.quickBtn} className={style.quickBtn}
@@ -75,7 +88,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
</Tooltip> </Tooltip>
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}> <Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
<Button <Button
onClick={() => handleCreateEvent('delay')} onClick={() => handleCreateEvent(SupportedEvent.Delay)}
size='xs' size='xs'
variant='ontime-subtle-white' variant='ontime-subtle-white'
disabled={disableAddDelay} disabled={disableAddDelay}
@@ -86,7 +99,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
</Tooltip> </Tooltip>
<Tooltip label='Add Block' openDelay={tooltipDelayMid}> <Tooltip label='Add Block' openDelay={tooltipDelayMid}>
<Button <Button
onClick={() => handleCreateEvent('block')} onClick={() => handleCreateEvent(SupportedEvent.Block)}
size='xs' size='xs'
variant='ontime-subtle-white' variant='ontime-subtle-white'
disabled={disableAddBlock} disabled={disableAddBlock}
+3
View File
@@ -1,3 +1,6 @@
export const tooltipDelaySlow = 1000; export const tooltipDelaySlow = 1000;
export const tooltipDelayMid = 500 export const tooltipDelayMid = 500
export const tooltipDelayFast = 300; export const tooltipDelayFast = 300;
export const queryRefetchInterval = 10000;
export const queryRefetchIntervalSlow = 30000;
+1 -1
View File
@@ -26,7 +26,7 @@ $active-indicator: #899948;
// interface panels // interface panels
$bg-container-l1: $gray-1350; $bg-container-l1: $gray-1350;
$bg-container-l2: $gray-1200; $bg-container-l2: $gray-1300;
$bg-container-l3: $gray-1350; $bg-container-l3: $gray-1350;
$box-shadow-l1: rgba(0, 0, 0, 0.15) 0 3px 3px 0; $box-shadow-l1: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
+1
View File
@@ -3,6 +3,7 @@ export const ontimeMenuOnDark = {
borderRadius: "3px", borderRadius: "3px",
border: 'none', border: 'none',
bg: '#fff', // $gray-50 bg: '#fff', // $gray-50
zIndex: 100,
}, },
item: { item: {
letterSpacing: '0.15px', letterSpacing: '0.15px',
+7 -1
View File
@@ -1,5 +1,6 @@
import sentryVitePlugin from '@sentry/vite-plugin'; import sentryVitePlugin from '@sentry/vite-plugin';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import svgrPlugin from 'vite-plugin-svgr'; import svgrPlugin from 'vite-plugin-svgr';
import viteTsconfigPaths from 'vite-tsconfig-paths'; import viteTsconfigPaths from 'vite-tsconfig-paths';
@@ -30,5 +31,10 @@ export default defineConfig({
build: { build: {
outDir: './build', outDir: './build',
sourcemap: true, sourcemap: true,
} },
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
}); });
+1 -1
View File
@@ -179,7 +179,7 @@ export async function reorderEvent(eventId, from, to) {
rundown.splice(to, 0, reorderedItem); rundown.splice(to, 0, reorderedItem);
// save rundown // save rundown
await DataProvider.setEventData(rundown); await DataProvider.setRundown(rundown);
updateTimer(); updateTimer();
return reorderedItem; return reorderedItem;