mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-26 01:19:11 +00:00
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:
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
+111
-59
@@ -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],
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user