Merge remote-tracking branch 'org/master' into google-sheets-lite

This commit is contained in:
arc-alex
2023-11-19 18:28:57 +01:00
58 changed files with 436 additions and 254 deletions
+1 -2
View File
@@ -2,8 +2,7 @@
export const PROJECT_DATA = ['project'];
export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields'];
export const RUNDOWN_TABLE_KEY = 'rundown';
export const RUNDOWN_TABLE = [RUNDOWN_TABLE_KEY];
export const RUNDOWN = ['rundown'];
export const APP_INFO = ['appinfo'];
export const OSC_SETTINGS = ['oscSettings'];
export const APP_SETTINGS = ['appSettings'];
+8 -8
View File
@@ -42,12 +42,12 @@ export function logAxiosError(prepend: string, error: unknown) {
* Utility function invalidates react-query caches
*/
export async function invalidateAllCaches() {
await ontimeQueryClient.invalidateQueries(['project']);
await ontimeQueryClient.invalidateQueries(['aliases']);
await ontimeQueryClient.invalidateQueries(['userFields']);
await ontimeQueryClient.invalidateQueries(['rundown']);
await ontimeQueryClient.invalidateQueries(['appinfo']);
await ontimeQueryClient.invalidateQueries(['oscSettings']);
await ontimeQueryClient.invalidateQueries(['appSettings']);
await ontimeQueryClient.invalidateQueries(['viewSettings']);
await ontimeQueryClient.invalidateQueries({ queryKey: ['project'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['aliases'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['userFields'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['rundown'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['appinfo'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['oscSettings'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['appSettings'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['viewSettings'] });
}
+11 -1
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import { OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import { GetRundownCached, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import { rundownURL } from './apiConstants';
@@ -7,6 +7,16 @@ import { rundownURL } from './apiConstants';
* @description HTTP request to fetch all events
* @return {Promise}
*/
export async function fetchCachedRundown(): Promise<GetRundownCached> {
const res = await axios.get(`${rundownURL}/cached`);
return res.data;
}
/**
* @deprecated use fetchCachedRundown instead
* @description HTTP request to fetch all events
* @return {Promise}
*/
export async function fetchRundown(): Promise<OntimeRundown> {
const res = await axios.get(rundownURL);
return res.data;
+2 -2
View File
@@ -2,6 +2,7 @@ import axios, { AxiosResponse } from 'axios';
import {
Alias,
DatabaseModel,
GetInfo,
OntimeRundown,
OSCSettings,
OscSubscription,
@@ -13,7 +14,6 @@ import {
import { ExcelImportMap } from 'ontime-utils';
import { apiRepoLatest } from '../../externals';
import { InfoType } from '../models/Info';
import fileDownload from '../utils/fileDownload';
import { ontimeURL } from './apiConstants';
@@ -39,7 +39,7 @@ export async function postSettings(data: Settings) {
* @description HTTP request to retrieve application info
* @return {Promise}
*/
export async function getInfo(): Promise<InfoType> {
export async function getInfo(): Promise<GetInfo> {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
}
@@ -19,9 +19,11 @@ interface TextInputProps extends BaseProps {
isTextArea?: false;
}
type ResizeOptions = 'horizontal' | 'vertical' | 'none';
interface TextAreaProps extends BaseProps {
isTextArea: true;
resize?: 'horizontal' | 'vertical' | 'none';
resize?: ResizeOptions;
}
type InputProps = TextInputProps | TextAreaProps;
@@ -35,7 +37,7 @@ export default function TextInput(props: InputProps) {
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
let resize = 'none';
let resize: ResizeOptions = 'none';
if (isTextArea) {
resize = (props as TextAreaProps)?.resize ?? 'none';
}
@@ -25,7 +25,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
const [operatorAuth, setOperatorAuth] = useState(true);
useEffect(() => {
if (status === 'loading') return;
if (status === 'pending') return;
if (!data) return;
const previousEditor = sessionStorage.getItem(storageKeys.editor);
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { GetInfo } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { APP_INFO } from '../api/apiConstants';
@@ -6,7 +7,7 @@ import { getInfo } from '../api/ontimeApi';
import { ontimePlaceholderInfo } from '../models/Info';
export default function useInfo() {
const { data, status, isError, refetch } = useQuery({
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
queryKey: APP_INFO,
queryFn: getInfo,
placeholderData: ontimePlaceholderInfo,
@@ -16,5 +17,5 @@ export default function useInfo() {
networkMode: 'always',
});
return { data, status, isError, refetch };
return { data, status, isError, refetch, isFetching };
}
@@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-nocheck -- working on it
import { useMutation, useQuery } from '@tanstack/react-query';
import { OSCSettings } from 'ontime-types';
@@ -24,20 +26,20 @@ export default function useOscSettings() {
}
export function useOscSettingsMutation() {
const { isLoading, mutateAsync } = useMutation({
const { isPending, mutateAsync } = useMutation({
mutationFn: postOSC,
onError: (error) => logAxiosError('Error saving OSC settings', error),
onSuccess: (res) => ontimeQueryClient.setQueryData(OSC_SETTINGS, res.data),
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
});
return { isLoading, mutateAsync };
return { isPending, mutateAsync };
}
export function usePostOscSubscriptions() {
const { isLoading, mutateAsync } = useMutation({
const { isPending, mutateAsync } = useMutation({
mutationFn: postOscSubscriptions,
onError: (error) => logAxiosError('Error saving OSC settings', error),
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
});
return { isLoading, mutateAsync };
return { isPending, mutateAsync };
}
@@ -1,19 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { GetRundownCached } from 'ontime-types';
import { queryRefetchInterval } from '../../ontimeConfig';
import { RUNDOWN_TABLE } from '../api/apiConstants';
import { fetchRundown } from '../api/eventsApi';
import { RUNDOWN } from '../api/apiConstants';
import { fetchCachedRundown } from '../api/eventsApi';
const cachedRundownPlaceholder = { rundown: [], revision: -1 };
// TODO: can we leverage structural sharing to see if data has changed?
export default function useRundown() {
const { data, status, isError, refetch } = useQuery({
queryKey: RUNDOWN_TABLE,
queryFn: fetchRundown,
placeholderData: [],
const { data, status, isError, refetch, isFetching } = useQuery<GetRundownCached>({
queryKey: RUNDOWN,
queryFn: fetchCachedRundown,
placeholderData: cachedRundownPlaceholder,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchInterval,
networkMode: 'always',
// structuralSharing: (oldData: GetRundownCached | undefined, newData: GetRundownCached) => {
// if (oldData === undefined) {
// return cachedRundownPlaceholder;
// }
// const hasDataChanged = oldData?.revision === newData.revision;
// return hasDataChanged ? oldData : newData;
// },
});
return { data, status, isError, refetch };
return { data: data?.rundown ?? [], status, isError, refetch, isFetching };
}
+71 -48
View File
@@ -1,9 +1,9 @@
import { useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import { GetRundownCached, isOntimeEvent, OntimeRundownEntry } from 'ontime-types';
import { getCueCandidate, swapOntimeEvents } from 'ontime-utils';
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
import { RUNDOWN } from '../api/apiConstants';
import { logAxiosError } from '../api/apiUtils';
import {
ReorderEntry,
@@ -36,7 +36,7 @@ export const useEventAction = () => {
// Fetch anyway, just to be sure
mutationFn: requestPostEvent,
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
networkMode: 'always',
});
@@ -67,8 +67,10 @@ export const useEventAction = () => {
after: options?.after,
};
const rundown = queryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? [];
if (newEvent?.cue === undefined) {
newEvent.cue = getCueCandidate(queryClient.getQueryData(RUNDOWN_TABLE) || [], options?.after);
newEvent.cue = getCueCandidate(rundown, options?.after);
}
// hard coding duration value to be as expected for now
@@ -78,7 +80,6 @@ export const useEventAction = () => {
}
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
if (previousEvent !== undefined && previousEvent.type === 'event') {
newEvent.timeStart = previousEvent.timeEnd;
@@ -115,25 +116,35 @@ export const useEventAction = () => {
// we optimistically update here
onMutate: async (newEvent) => {
// cancel ongoing queries
await queryClient.cancelQueries([RUNDOWN_TABLE_KEY, newEvent.id]);
await queryClient.cancelQueries({ queryKey: RUNDOWN });
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([RUNDOWN_TABLE_KEY, newEvent.id]);
// optimistically update object
queryClient.setQueryData([RUNDOWN_TABLE_KEY, newEvent.id], newEvent);
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
if (previousData) {
// optimistically update object
const optimisticRundown = [...previousData.rundown];
const index = optimisticRundown.findIndex((event) => event.id === newEvent.id);
if (index > -1) {
// @ts-expect-error -- we expect the event types to match
optimisticRundown[index] = { ...optimisticRundown[index], ...newEvent };
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
}
}
// Return a context with the previous and new events
return { previousEvent, newEvent };
return { previousData, newEvent };
},
// Mutation fails, rollback undoes optimist update
onError: (_error, _newEvent, context) => {
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
queryClient.setQueryData(RUNDOWN, context?.previousData);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: async () => {
await queryClient.invalidateQueries([RUNDOWN_TABLE_KEY]);
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
networkMode: 'always',
});
@@ -161,28 +172,37 @@ export const useEventAction = () => {
// we optimistically update here
onMutate: async (eventId) => {
// cancel ongoing queries
await queryClient.cancelQueries([RUNDOWN_TABLE_KEY, eventId]);
await queryClient.cancelQueries({ queryKey: RUNDOWN });
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
const filtered = [...(previousEvents as OntimeRundown)].filter((e) => e.id !== eventId);
if (previousData) {
// optimistically update object
const optimisticRundown = [...previousData.rundown];
const index = optimisticRundown.findIndex((event) => event.id === eventId);
if (index > -1) {
optimisticRundown.splice(index, 1);
// optimistically update object
queryClient.setQueryData(RUNDOWN_TABLE, filtered);
queryClient.setQueryData(RUNDOWN, {
rundown: optimisticRundown,
revision: -1,
});
}
}
// Return a context with the previous and new events
return { previousEvents };
return { previousData };
},
// Mutation fails, rollback undoes optimist update
onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
queryClient.setQueryData(RUNDOWN, context?.previousData);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
networkMode: 'always',
});
@@ -210,26 +230,26 @@ export const useEventAction = () => {
// we optimistically update here
onMutate: async () => {
// cancel ongoing queries
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
await queryClient.cancelQueries({ queryKey: RUNDOWN });
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
// optimistically update object
queryClient.setQueryData(RUNDOWN_TABLE, []);
queryClient.setQueryData(RUNDOWN, { rundown: [], revision: -1 });
// Return a context with the previous and new events
return { previousEvents };
return { previousData };
},
// Mutation fails, rollback undos optimist update
onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
queryClient.setQueryData(RUNDOWN, context?.previousData);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
networkMode: 'always',
});
@@ -253,7 +273,7 @@ export const useEventAction = () => {
mutationFn: requestApplyDelay,
// Mutation finished, failed or successful
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
networkMode: 'always',
});
@@ -281,30 +301,32 @@ export const useEventAction = () => {
// we optimistically update here
onMutate: async (data) => {
// cancel ongoing queries
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
await queryClient.cancelQueries({ queryKey: RUNDOWN });
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
const e = [...(previousEvents as OntimeRundown)];
const [reorderedItem] = e.splice(data.from, 1);
e.splice(data.to, 0, reorderedItem);
if (previousData) {
// optimistically update object
const optimisticRundown = [...previousData.rundown];
const [reorderedItem] = optimisticRundown.splice(data.from, 1);
optimisticRundown.splice(data.to, 0, reorderedItem);
// optimistically update object
queryClient.setQueryData(RUNDOWN_TABLE, e);
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
}
// Return a context with the previous and new events
return { previousEvents };
return { previousData };
},
// Mutation fails, rollback undoes optimist update
onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
queryClient.setQueryData(RUNDOWN, context?.previousData);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
networkMode: 'always',
});
@@ -337,31 +359,32 @@ export const useEventAction = () => {
// we optimistically update here
onMutate: async ({ from, to }) => {
// cancel ongoing queries
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
await queryClient.cancelQueries({ queryKey: RUNDOWN });
// Snapshot the previous value
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
if (previousData) {
// optimistically update object
const fromEventIndex = previousData.rundown.findIndex((event) => event.id === from);
const toEventIndex = previousData.rundown.findIndex((event) => event.id === to);
const fromEventIndex = rundown.findIndex((event) => event.id === from);
const toEventIndex = rundown.findIndex((event) => event.id === to);
const optimisticRundown = swapOntimeEvents(previousData.rundown, fromEventIndex, toEventIndex);
const previousEvents = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
// optimistically update object
queryClient.setQueryData(RUNDOWN_TABLE, previousEvents);
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
}
// Return a context with the previous events
return { previousEvents };
return { previousData };
},
// Mutation fails, rollback undoes optimist update
onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
queryClient.setQueryData(RUNDOWN, context?.previousData);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
networkMode: 'always',
});
@@ -21,7 +21,7 @@ interface UseFollowComponentProps {
scrollRef: MutableRefObject<HTMLElement | null>;
doFollow: boolean;
topOffset?: number;
setScrollFlag?: () => void;
setScrollFlag?: (newValue: boolean) => void;
}
export default function useFollowComponent(props: UseFollowComponentProps) {
@@ -34,14 +34,15 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
}
if (followRef.current && scrollRef.current) {
setScrollFlag?.(true);
// Use requestAnimationFrame to ensure the component is fully loaded
window.requestAnimationFrame(() => {
setScrollFlag?.();
scrollToComponent(
followRef as MutableRefObject<HTMLElement>,
scrollRef as MutableRefObject<HTMLElement>,
topOffset,
);
setScrollFlag?.(false);
});
}
@@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-nocheck -- working on it
import { useCallback, useEffect, useState } from 'react';
interface WebkitDocument extends Document {
+22 -16
View File
@@ -1,19 +1,25 @@
import { Settings } from 'ontime-types';
import { GetInfo, OSCSettings } from 'ontime-types';
type NetworkInterfaceType = {
name: string;
address: string;
};
export type InfoType = {
networkInterfaces: NetworkInterfaceType[];
settings: Pick<Settings, 'version' | 'serverPort'>;
};
export const ontimePlaceholderInfo: InfoType = {
networkInterfaces: [],
settings: {
version: 2,
serverPort: 4001,
export const oscPlaceholderSettings: OSCSettings = {
portIn: 0,
portOut: 0,
targetIP: '',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: [],
onStart: [],
onPause: [],
onStop: [],
onUpdate: [],
onFinish: [],
},
};
export const ontimePlaceholderInfo: GetInfo = {
networkInterfaces: [],
version: '2.0.0',
serverPort: 4001,
osc: oscPlaceholderSettings,
cssOverride: '',
};
@@ -2,7 +2,7 @@ import { Settings } from 'ontime-types';
export const ontimePlaceholderSettings: Settings = {
app: 'ontime',
version: 2,
version: '2.0.0',
serverPort: 4001,
editorKey: null,
operatorKey: null,
+1 -1
View File
@@ -3,7 +3,7 @@ import { QueryClient } from '@tanstack/react-query';
export const ontimeQueryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: 1000 * 60 * 10, // 10 min
gcTime: 1000 * 60 * 10, // 10 min
},
},
});
@@ -1,4 +1,4 @@
import { memo } from 'react';
import { memo, ReactNode } from 'react';
import { Button, Checkbox, Switch } from '@chakra-ui/react';
import { Column } from '@tanstack/react-table';
import { OntimeRundownEntry } from 'ontime-types';
@@ -44,7 +44,7 @@ function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
defaultChecked={visible}
onChange={column.getToggleVisibilityHandler()}
/>
{columnHeader}
{columnHeader as ReactNode}
</label>
);
})}
@@ -9,7 +9,7 @@ import { millisToString } from 'ontime-utils';
* @return {string}
*/
export const parseField = (field: keyof OntimeRundown, data: unknown): string => {
export const parseField = <T extends OntimeEntryCommonKeys>(field: T, data: unknown): string => {
let val;
switch (field) {
case 'timeStart':
@@ -96,6 +96,7 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, userF
rundown.forEach((entry) => {
const row: string[] = [];
// @ts-expect-error -- not sure how to type this
fieldOrder.forEach((field) => row.push(parseField(field, entry[field])));
data.push(row);
});
@@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-nocheck -- working on it
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { FormControl, Input, Switch } from '@chakra-ui/react';
@@ -18,7 +18,7 @@ import {
} from '@chakra-ui/react';
import type { ProjectData } from 'ontime-types';
import { PROJECT_DATA, RUNDOWN_TABLE } from '../../../common/api/apiConstants';
import { PROJECT_DATA, RUNDOWN } from '../../../common/api/apiConstants';
import { postNew } from '../../../common/api/ontimeApi';
import useProjectData from '../../../common/hooks-query/useProjectData';
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
@@ -52,8 +52,8 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
const onSubmit = async (data: Partial<ProjectData>) => {
try {
await postNew(data);
await ontimeQueryClient.invalidateQueries(PROJECT_DATA);
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
await ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_DATA });
await ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
onClose();
} catch (_) {
@@ -48,7 +48,7 @@ export default function AliasesForm() {
useEffect(() => {
if (data) {
reset(data);
reset({ aliases: data });
}
}, [data, reset]);
@@ -78,7 +78,7 @@ export default function AliasesForm() {
});
};
const disableInputs = status === 'loading';
const disableInputs = status === 'pending';
const hasTooManyOptions = fields.length >= 20;
if (isFetching) {
@@ -50,7 +50,7 @@ export default function AppSettingsModal() {
reset(data);
};
const disableInputs = status === 'loading';
const disableInputs = status === 'pending';
if (isFetching) {
return <ModalLoader />;
@@ -87,7 +87,7 @@ export default function AppSettingsModal() {
description='Protect the editor with a pin code'
error={errors.editorKey?.message}
>
<ModalPinInput register={register} formName='editorKey' isDisabled={disableInputs} />
<ModalPinInput register={register as any} formName='editorKey' isDisabled={disableInputs} />
</ModalSplitInput>
<ModalSplitInput
field='operatorKey'
@@ -95,7 +95,7 @@ export default function AppSettingsModal() {
description='Protect the cuesheet with a pin code'
error={errors.operatorKey?.message}
>
<ModalPinInput register={register} formName='operatorKey' isDisabled={disableInputs} />
<ModalPinInput register={register as any} formName='operatorKey' isDisabled={disableInputs} />
</ModalSplitInput>
<div style={{ height: '16px' }} />
<ModalSplitInput
@@ -51,7 +51,7 @@ export default function CuesheetSettingsForm() {
reset(data);
};
const disableInputs = status === 'loading';
const disableInputs = status === 'pending';
if (isFetching) {
return <ModalLoader />;
@@ -48,7 +48,7 @@ export default function ProjectDataForm() {
reset(data);
};
const disableInputs = status === 'loading';
const disableInputs = status === 'pending';
if (isFetching) {
return <ModalLoader />;
@@ -3,19 +3,24 @@
.aliases {
display: flex;
align-items: center;
gap: 8px;
gap: 0.5rem;
flex-direction: column;
width: 100%;
padding: 8px 0;
padding: 0.5rem 0;
.aliasRow {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
gap: 0.5rem;
}
.grow {
flex: 1;
}
}
.url {
font-size: calc(1rem - 2px);
user-select: text;
}
@@ -1,16 +1,18 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Input, Switch } from '@chakra-ui/react';
import { Alert, AlertDescription, AlertIcon, AlertTitle, Input, Switch } from '@chakra-ui/react';
import { ViewSettings } from 'ontime-types';
import { logAxiosError } from '../../../common/api/apiUtils';
import { postViewSettings } from '../../../common/api/ontimeApi';
import { PopoverPickerRHF } from '../../../common/components/input/popover-picker/PopoverPicker';
import useInfo from '../../../common/hooks-query/useInfo';
import useViewSettings from '../../../common/hooks-query/useViewSettings';
import { mtm } from '../../../common/utils/timeConstants';
import ModalLoader from '../modal-loader/ModalLoader';
import { inputProps } from '../modalHelper';
import ModalInput from '../ModalInput';
import ModalLink from '../ModalLink';
import ModalSplitInput from '../ModalSplitInput';
import OntimeModalFooter from '../OntimeModalFooter';
@@ -18,8 +20,12 @@ import InputMillisWithString from './InputMillisWithString';
import style from './SettingsModal.module.scss';
const cssOverrideDocsUrl = 'https://ontime.gitbook.io/v2/features/custom-styling';
export default function ViewSettingsForm() {
const { data, status, refetch, isFetching } = useViewSettings();
const { data: info, isFetching: isFetchingInfo } = useInfo();
const {
control,
handleSubmit,
@@ -73,15 +79,26 @@ export default function ViewSettingsForm() {
return null;
}
const disableInputs = status === 'loading';
const disableInputs = status === 'pending';
if (isFetching) {
if (isFetching || isFetchingInfo) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} id='view-settings' className={style.sectionContainer}>
<span className={style.title}>General view settings</span>
<Alert status='info' variant='ontime-on-light-info'>
<AlertIcon />
<div className={style.column}>
<AlertTitle>CSS Override</AlertTitle>
<AlertDescription>
Ontime will use the CSS file at its install location. <br />
<span className={style.url}>{info?.cssOverride}</span>
<ModalLink href={cssOverrideDocsUrl}>For more information, see the docs</ModalLink>
</AlertDescription>
</div>
</Alert>
<ModalSplitInput
field='overrideStyles'
title='Override CSS Styles'
@@ -13,7 +13,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { OntimeRundown, ProjectData, UserFields } from 'ontime-types';
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
import { PROJECT_DATA, RUNDOWN_TABLE, USERFIELDS } from '../../../common/api/apiConstants';
import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils';
import {
patchData,
@@ -155,11 +155,11 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
setSubmitting(true);
try {
await patchData({ rundown, userFields, project });
queryClient.setQueryData(RUNDOWN_TABLE, rundown);
queryClient.setQueryData(RUNDOWN, { rundown, revision: -1 });
queryClient.setQueryData(USERFIELDS, userFields);
queryClient.setQueryData(PROJECT_DATA, project);
await queryClient.invalidateQueries({
queryKey: [...RUNDOWN_TABLE, ...USERFIELDS, ...PROJECT_DATA],
queryKey: [...RUNDOWN, ...USERFIELDS, ...PROJECT_DATA],
});
doClose = true;
} catch (error) {
+11 -11
View File
@@ -12,6 +12,7 @@ import { useOperator } from '../../common/hooks/useSocket';
import useProjectData from '../../common/hooks-query/useProjectData';
import useRundown from '../../common/hooks-query/useRundown';
import useUserFields from '../../common/hooks-query/useUserFields';
import { debounce } from '../../common/utils/debounce';
import { isStringBoolean } from '../../common/utils/viewUtils';
import FollowButton from './follow-button/FollowButton';
@@ -32,7 +33,6 @@ export default function Operator() {
const featureData = useOperator();
const [searchParams] = useSearchParams();
const isAutomatedScroll = useRef(false);
const [lockAutoScroll, setLockAutoScroll] = useState(false);
const selectedRef = useRef<HTMLDivElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
@@ -41,7 +41,6 @@ export default function Operator() {
scrollRef: scrollRef,
doFollow: !lockAutoScroll,
topOffset: selectedOffset,
setScrollFlag: () => (isAutomatedScroll.current = true),
});
// Set window title
@@ -65,13 +64,8 @@ export default function Operator() {
setLockAutoScroll(false);
};
const handleScroll = () => {
// prevent considering automated scrolls as user scrolls
if (isAutomatedScroll.current) {
isAutomatedScroll.current = false;
return;
}
// prevent considering automated scrolls as user scrolls
const handleUserScroll = () => {
if (selectedRef?.current && scrollRef?.current) {
const selectedRect = selectedRef.current.getBoundingClientRect();
const scrollerRect = scrollRef.current.getBoundingClientRect();
@@ -82,9 +76,10 @@ export default function Operator() {
}
}
};
const debouncedHandleScroll = debounce(handleUserScroll, 1000);
const missingData = !data || !userFields || !projectData;
const isLoading = status === 'loading' || userFieldsStatus === 'loading' || projectDataStatus === 'loading';
const isLoading = status === 'pending' || userFieldsStatus === 'pending' || projectDataStatus === 'pending';
if (missingData || isLoading) {
return <Empty text='Loading...' />;
@@ -119,7 +114,12 @@ export default function Operator() {
lastId={lastEvent?.id}
/>
<div className={style.operatorEvents} onScroll={handleScroll} ref={scrollRef}>
<div
className={style.operatorEvents}
onWheel={debouncedHandleScroll}
onTouchMove={debouncedHandleScroll}
ref={scrollRef}
>
{data.map((entry) => {
if (isOntimeEvent(entry)) {
const isSelected = featureData.selectedEventId === entry.id;
@@ -1,8 +1,8 @@
import { useCallback } from 'react';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
import { GetRundownCached, OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
import { calculateDuration, getCueCandidate } from 'ontime-utils';
import { RUNDOWN_TABLE } from '../../common/api/apiConstants';
import { RUNDOWN } from '../../common/api/apiConstants';
import { useEventAction } from '../../common/hooks/useEventAction';
import { ontimeQueryClient } from '../../common/queryClient';
import { useAppMode } from '../../common/stores/appModeStore';
@@ -100,7 +100,8 @@ export default function RundownEntry(props: RundownEntryProps) {
}
case 'clone': {
const newEvent = cloneEvent(data as OntimeEvent, data.id);
newEvent.cue = getCueCandidate(ontimeQueryClient.getQueryData(RUNDOWN_TABLE) || [], data.id);
const rundown = ontimeQueryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? []
newEvent.cue = getCueCandidate(rundown, data.id);
addEvent(newEvent);
break;
}
@@ -13,7 +13,7 @@ import { EventItemActions } from '../../RundownEntry';
interface BlockActionMenuProps {
enableDelete?: boolean;
showClone?: boolean;
actionHandler: (action: EventItemActions, payload?: unknown) => void;
actionHandler: (action: EventItemActions, payload?: any) => void;
className?: string;
}
@@ -1,6 +1,6 @@
/* eslint-disable react/display-name */
import { ComponentType, useMemo } from 'react';
import { SupportedEvent } from 'ontime-types';
import { TimeManagerType } from 'common/models/TimeManager.type';
import { Message, OntimeEvent, ProjectData, SupportedEvent, TimerMessage, ViewSettings } from 'ontime-types';
import { useStore } from 'zustand';
import useProjectData from '../../common/hooks-query/useProjectData';
@@ -9,8 +9,32 @@ import useViewSettings from '../../common/hooks-query/useViewSettings';
import { runtime } from '../../common/stores/runtime';
import { useViewOptionsStore } from '../../common/stores/viewOptions';
const withData = <P extends object>(Component: ComponentType<P>) => {
return (props: Partial<P>) => {
type WithDataProps = {
isMirrored: boolean;
pres: TimerMessage;
publ: Message;
lower: Message;
eventNow: OntimeEvent | null;
publicEventNow: OntimeEvent | null;
eventNext: OntimeEvent | null;
publicEventNext: OntimeEvent | null;
time: TimeManagerType;
events: OntimeEvent[];
backstageEvents: OntimeEvent[];
selectedId: string | null;
publicSelectedId: string | null;
nextId: string | null;
general: ProjectData;
viewSettings: ViewSettings;
onAir: boolean;
};
function getDisplayName(Component: React.ComponentType<any>): string {
return Component.displayName || Component.name || 'Component';
}
const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
const WithDataComponent = (props: P) => {
// persisted app state
const isMirrored = useViewOptionsStore((state) => state.mirror);
@@ -84,6 +108,9 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
/>
);
};
WithDataComponent.displayName = `WithData(${getDisplayName(Component)})`;
return WithDataComponent;
};
export default withData;
@@ -121,7 +121,9 @@ export default function Countdown(props: CountdownProps) {
<div className='time'>{clock}</div>
</div>
<div className='status'>{getLocalizedString(`countdown.${runningMessage}`)}</div>
{runningMessage !== TimerMessage.unhandled && (
<div className='status'>{getLocalizedString(`countdown.${runningMessage}`)}</div>
)}
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
{formattedTimer}