mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 21:24:11 +00:00
Merge branch 'master' into feat/http-integration
This commit is contained in:
@@ -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 HTTP_SETTINGS = ['httpSettings'];
|
||||
|
||||
@@ -2,18 +2,32 @@ import axios, { AxiosError } from 'axios';
|
||||
import { LogLevel } from 'ontime-types';
|
||||
import { generateId, millisToString } from 'ontime-utils';
|
||||
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
import { addLog } from '../stores/logger';
|
||||
import { nowInMillis } from '../utils/time';
|
||||
|
||||
export function logAxiosError(prepend: string, error: unknown) {
|
||||
let message;
|
||||
export function maybeAxiosError(error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const statusText = (error as AxiosError).response?.statusText ?? '';
|
||||
const data = (error as AxiosError).response?.data ?? '';
|
||||
message = `${prepend} ${statusText}: ${data}`;
|
||||
let data = (error as AxiosError).response?.data ?? '';
|
||||
if (typeof data === 'object') {
|
||||
if ('message' in data) {
|
||||
data = JSON.stringify(data.message);
|
||||
} else {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
return `${statusText}: ${data}`;
|
||||
} else {
|
||||
message = `${prepend}: ${error}`;
|
||||
if (typeof error !== 'string') {
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
export function logAxiosError(prepend: string, error: unknown) {
|
||||
const message = `${prepend}: ${maybeAxiosError(error)}`;
|
||||
|
||||
addLog({
|
||||
id: generateId(),
|
||||
@@ -23,3 +37,17 @@ export function logAxiosError(prepend: string, error: unknown) {
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function invalidates react-query caches
|
||||
*/
|
||||
export async function invalidateAllCaches() {
|
||||
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'] });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import {
|
||||
Alias,
|
||||
OSCSettings,
|
||||
Subscription,
|
||||
DatabaseModel,
|
||||
GetInfo,
|
||||
OntimeRundown,
|
||||
ProjectData,
|
||||
Settings,
|
||||
UserFields,
|
||||
ViewSettings,
|
||||
HTTPSettings,
|
||||
} from 'ontime-types';
|
||||
import { ExcelImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiRepoLatest } from '../../externals';
|
||||
import { InfoType } from '../models/Info';
|
||||
import fileDownload from '../utils/fileDownload';
|
||||
|
||||
import { ontimeURL } from './apiConstants';
|
||||
@@ -37,7 +40,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;
|
||||
}
|
||||
@@ -152,17 +155,25 @@ export const downloadRundown = () => {
|
||||
return fileDownload(ontimeURL, { name: 'rundown', type: 'json' }, { type: 'application/json;charset=utf-8;' });
|
||||
};
|
||||
|
||||
// TODO: should this be extracted to shared code?
|
||||
export type ProjectFileImportOptions = {
|
||||
onlyRundown: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to upload events db
|
||||
* @return {Promise}
|
||||
*/
|
||||
type UploadDataOptions = {
|
||||
onlyRundown?: boolean;
|
||||
};
|
||||
export const uploadData = async (file: File, setProgress: (value: number) => void, options?: UploadDataOptions) => {
|
||||
export const uploadProjectFile = async (
|
||||
file: File,
|
||||
setProgress: (value: number) => void,
|
||||
options?: Partial<ProjectFileImportOptions>,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
const onlyRundown = options?.onlyRundown || 'false';
|
||||
|
||||
const onlyRundown = Boolean(options?.onlyRundown);
|
||||
|
||||
await axios
|
||||
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
|
||||
headers: {
|
||||
@@ -176,6 +187,47 @@ export const uploadData = async (file: File, setProgress: (value: number) => voi
|
||||
.then((response) => response.data.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Make patch changes to the objects in the db
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function patchData(patchDb: Partial<DatabaseModel>) {
|
||||
const response = await axios.patch(`${ontimeURL}/db`, patchDb);
|
||||
return response;
|
||||
}
|
||||
|
||||
type PostPreviewExcelResponse = {
|
||||
rundown: OntimeRundown;
|
||||
project: ProjectData;
|
||||
userFields: UserFields;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Make patch changes to the objects in the db
|
||||
* @return {Promise} - returns parsed rundown and userfields
|
||||
*/
|
||||
export async function postPreviewExcel(file: File, setProgress: (value: number) => void, options?: ExcelImportMap) {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
formData.append('options', JSON.stringify(options));
|
||||
|
||||
const response: AxiosResponse<PostPreviewExcelResponse> = await axios.post(
|
||||
`${ontimeURL}/preview-spreadsheet`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0;
|
||||
setProgress(complete);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export type HasUpdate = {
|
||||
url: string;
|
||||
version: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { TitleActions } from '../../../../features/event-editor/composite/EventEditorDataLeft';
|
||||
import { EditorUpdateFields } from '../../../../features/event-editor/EventEditor';
|
||||
|
||||
import Swatch from './Swatch';
|
||||
|
||||
@@ -8,8 +8,8 @@ import style from './SwatchSelect.module.scss';
|
||||
|
||||
interface ColourInputProps {
|
||||
value: string;
|
||||
name: TitleActions;
|
||||
handleChange: (newValue: TitleActions, name: string) => void;
|
||||
name: EditorUpdateFields;
|
||||
handleChange: (newValue: EditorUpdateFields, name: string) => void;
|
||||
}
|
||||
|
||||
const colours = [
|
||||
|
||||
@@ -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,8 +25,14 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
||||
return (
|
||||
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
|
||||
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
||||
<div className='multiprogress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
|
||||
<div className='multiprogress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
|
||||
<div
|
||||
className='multiprogress-bar__bg-warning'
|
||||
style={{ width: `${warningWidth}%`, backgroundColor: warningColor }}
|
||||
/>
|
||||
<div
|
||||
className='multiprogress-bar__bg-danger'
|
||||
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
|
||||
/>
|
||||
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,6 @@ interface ScheduleProviderProps {
|
||||
events: OntimeEvent[];
|
||||
selectedEventId: string | null;
|
||||
isBackstage?: boolean;
|
||||
eventsPerPage?: number;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
@@ -30,7 +29,6 @@ export const ScheduleProvider = ({
|
||||
events,
|
||||
selectedEventId,
|
||||
isBackstage = false,
|
||||
eventsPerPage = 7,
|
||||
time = 10,
|
||||
}: PropsWithChildren<ScheduleProviderProps>) => {
|
||||
const [visiblePage, setVisiblePage] = useState(0);
|
||||
@@ -39,6 +37,7 @@ export const ScheduleProvider = ({
|
||||
// look for overrides from views
|
||||
const hidePast = isStringBoolean(searchParams.get('hidePast'));
|
||||
const stopCycle = isStringBoolean(searchParams.get('stopCycle'));
|
||||
const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? 7);
|
||||
|
||||
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
||||
|
||||
|
||||
@@ -14,16 +14,7 @@ interface ScheduleItemProps {
|
||||
}
|
||||
|
||||
export default function ScheduleItem(props: ScheduleItemProps) {
|
||||
const {
|
||||
selected,
|
||||
timeStart,
|
||||
timeEnd,
|
||||
title,
|
||||
presenter,
|
||||
backstageEvent,
|
||||
colour,
|
||||
skip,
|
||||
} = props;
|
||||
const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props;
|
||||
|
||||
const start = formatTime(timeStart, { format: 'hh:mm' });
|
||||
const end = formatTime(timeEnd, { format: 'hh:mm' });
|
||||
@@ -37,9 +28,7 @@ export default function ScheduleItem(props: ScheduleItemProps) {
|
||||
{`${start} → ${end} ${backstageEvent ? '*' : ''}`}
|
||||
</div>
|
||||
<div className='entry-title'>{title}</div>
|
||||
{presenter && (
|
||||
<div className='entry-presenter'>{presenter}</div>
|
||||
)}
|
||||
{presenter && <div className='entry-presenter'>{presenter}</div>}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,11 @@ export default function ScheduleNav({ className }: ScheduleNavProps) {
|
||||
<div className={`schedule-nav ${className}`}>
|
||||
{numPages > 1 &&
|
||||
[...Array(numPages).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<div
|
||||
key={i}
|
||||
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormEvent, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
@@ -12,11 +12,27 @@ import {
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { useLocalStorage } from '../../../common/hooks/useLocalStorage';
|
||||
|
||||
import ParamInput from './ParamInput';
|
||||
import { ParamField } from './types';
|
||||
|
||||
import style from './ViewParamsEditor.module.scss';
|
||||
|
||||
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
|
||||
type SavedViewParams = Record<string, ViewParamsObj>;
|
||||
|
||||
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj) =>
|
||||
Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => {
|
||||
if (typeof value === 'string' && value.length) {
|
||||
newSearchParams.set(id, value);
|
||||
|
||||
return newSearchParams;
|
||||
}
|
||||
|
||||
return newSearchParams;
|
||||
}, new URLSearchParams());
|
||||
|
||||
interface EditFormDrawerProps {
|
||||
paramFields: ParamField[];
|
||||
}
|
||||
@@ -24,6 +40,8 @@ interface EditFormDrawerProps {
|
||||
export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const { pathname } = useLocation();
|
||||
const [storedViewParams, setStoredViewParams] = useLocalStorage<SavedViewParams>('ontime-views', {});
|
||||
|
||||
useEffect(() => {
|
||||
const isEditing = searchParams.get('edit');
|
||||
@@ -33,6 +51,19 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
}
|
||||
}, [searchParams, onOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const viewParamsObjFromLocalStorage = storedViewParams[pathname];
|
||||
|
||||
if (viewParamsObjFromLocalStorage !== undefined) {
|
||||
const defaultSearchParams = getURLSearchParamsFromObj(viewParamsObjFromLocalStorage);
|
||||
setSearchParams(defaultSearchParams);
|
||||
}
|
||||
|
||||
// linter is asking for `setSearchParams` in the useEffect deps
|
||||
// rule is disabled since adding `setSearchParams` results in unnecessary re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [storedViewParams, pathname]);
|
||||
|
||||
const onEditDrawerClose = () => {
|
||||
onClose();
|
||||
|
||||
@@ -41,6 +72,7 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
};
|
||||
|
||||
const clearParams = () => {
|
||||
setStoredViewParams({ ...storedViewParams, [pathname]: {} });
|
||||
setSearchParams();
|
||||
onClose();
|
||||
};
|
||||
@@ -49,15 +81,9 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
formEvent.preventDefault();
|
||||
|
||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||
const newSearchParams = Object.entries(newParamsObject).reduce((newSearchParams, [id, value]) => {
|
||||
if (typeof value === 'string' && value.length) {
|
||||
newSearchParams.set(id, value);
|
||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject);
|
||||
|
||||
return newSearchParams;
|
||||
}
|
||||
|
||||
return newSearchParams;
|
||||
}, new URLSearchParams());
|
||||
setStoredViewParams({ ...storedViewParams, [pathname]: newParamsObject });
|
||||
setSearchParams(newSearchParams);
|
||||
};
|
||||
|
||||
|
||||
@@ -208,6 +208,12 @@ export const BACKSTAGE_OPTIONS: ParamField[] = [
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
},
|
||||
];
|
||||
|
||||
export const PUBLIC_OPTIONS: ParamField[] = [
|
||||
@@ -224,6 +230,12 @@ export const PUBLIC_OPTIONS: ParamField[] = [
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
},
|
||||
];
|
||||
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ export default function useClickOutside<T extends HTMLElement = HTMLElement>(
|
||||
ref: RefObject<T>,
|
||||
callback: ClickOutsideEventHandler,
|
||||
) {
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(event: MouseEvent) {
|
||||
const element = ref?.current;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default function useElectronEvent() {
|
||||
const isElectron = window?.process?.type === 'renderer';
|
||||
|
||||
const sendToElectron = (channel: string, args?: string | Record<string, any>) => {
|
||||
const sendToElectron = (channel: string, args?: string | Record<string, unknown>) => {
|
||||
if (isElectron) {
|
||||
window?.ipcRenderer.send(channel, args);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -100,7 +101,7 @@ export const useEventAction = () => {
|
||||
// @ts-expect-error -- we know that the object is well formed now
|
||||
await _addEventMutation.mutateAsync(newEvent);
|
||||
} catch (error) {
|
||||
logAxiosError('Error fetching data', error);
|
||||
logAxiosError('Failed adding event', error);
|
||||
}
|
||||
},
|
||||
[_addEventMutation, defaultPublic, queryClient, startTimeIsLastEnd],
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
export type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
||||
|
||||
@@ -26,13 +20,13 @@ const LOG_LEVEL: Record<TLogLevel, number> = {
|
||||
};
|
||||
|
||||
const useFitText = ({
|
||||
logLevel: logLevelOption = 'info',
|
||||
maxFontSize = 100,
|
||||
minFontSize = 20,
|
||||
onFinish,
|
||||
onStart,
|
||||
resolution = 5,
|
||||
}: TOptions = {}) => {
|
||||
logLevel: logLevelOption = 'info',
|
||||
maxFontSize = 100,
|
||||
minFontSize = 20,
|
||||
onFinish,
|
||||
onStart,
|
||||
resolution = 5,
|
||||
}: TOptions = {}) => {
|
||||
const logLevel = LOG_LEVEL[logLevelOption];
|
||||
|
||||
const initState = useCallback(() => {
|
||||
@@ -112,8 +106,7 @@ const useFitText = ({
|
||||
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
|
||||
const isOverflow =
|
||||
!!ref.current &&
|
||||
(ref.current.scrollHeight > ref.current.offsetHeight ||
|
||||
ref.current.scrollWidth > ref.current.offsetWidth);
|
||||
(ref.current.scrollHeight > ref.current.offsetHeight || ref.current.scrollWidth > ref.current.offsetWidth);
|
||||
const isFailed = isOverflow && fontSize === fontSizePrev;
|
||||
const isAsc = fontSize > fontSizePrev;
|
||||
|
||||
@@ -123,9 +116,7 @@ const useFitText = ({
|
||||
if (isFailed) {
|
||||
isCalculatingRef.current = false;
|
||||
if (logLevel <= LOG_LEVEL.info) {
|
||||
console.info(
|
||||
`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`,
|
||||
);
|
||||
console.info(`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`);
|
||||
}
|
||||
} else if (isOverflow) {
|
||||
setState({
|
||||
@@ -160,16 +151,7 @@ const useFitText = ({
|
||||
fontSizeMin: newMin,
|
||||
fontSizePrev: fontSize,
|
||||
});
|
||||
}, [
|
||||
calcKey,
|
||||
fontSize,
|
||||
fontSizeMax,
|
||||
fontSizeMin,
|
||||
fontSizePrev,
|
||||
onFinish,
|
||||
ref,
|
||||
resolution,
|
||||
]);
|
||||
}, [calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev, onFinish, ref, resolution]);
|
||||
|
||||
return { fontSize: `${fontSize}%`, ref };
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* @description utility hook to around setInterval
|
||||
|
||||
@@ -46,7 +46,7 @@ export const useLocalStorage = <T>(key: string, initialValue: T): [T, (value: T
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
import { resolvePath } from 'react-router-dom';
|
||||
import { validateAlias, generateURLFromAlias, getAliasRoute } from '../aliases';
|
||||
|
||||
import { generateURLFromAlias, getAliasRoute, validateAlias } from '../aliases';
|
||||
|
||||
describe('An alias fails if incorrect', () => {
|
||||
const testsToFail = [
|
||||
|
||||
@@ -12,7 +12,7 @@ test('Clamps a set of numbers correctly', () => {
|
||||
{ num: -50, min: 0, max: 0, result: 0 },
|
||||
{ num: 50.5, min: 0, max: 100, result: 50.5 },
|
||||
{ num: 50, min: 0, max: 20.32, result: 20.32 },
|
||||
{ num: 10, min: 20.32, max: 40, result: 20.32 }
|
||||
{ num: 10, min: 20.32, max: 40, result: 20.32 },
|
||||
];
|
||||
|
||||
testCases.forEach((t) => expect(clamp(t.num, t.min, t.max)).toBe(t.result));
|
||||
|
||||
@@ -129,7 +129,7 @@ export const socketSend = (message: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const socketSendJson = (type: string, payload?: any) => {
|
||||
export const socketSendJson = (type: string, payload?: unknown) => {
|
||||
socketSend(
|
||||
JSON.stringify({
|
||||
type,
|
||||
|
||||
@@ -15,8 +15,8 @@ export const getAccessibleColour = (bgColour?: string): ColourCombination => {
|
||||
try {
|
||||
const textColor = Color(bgColour).isLight() ? 'black' : '#fffffa';
|
||||
return { backgroundColor: bgColour, color: textColor };
|
||||
} catch (error) {
|
||||
console.log(`Unable to parse colour: ${bgColour}`);
|
||||
} catch (_error) {
|
||||
/* we do not handle errors here */
|
||||
}
|
||||
}
|
||||
return { backgroundColor: '#000', color: '#fffffa' };
|
||||
|
||||
Reference in New Issue
Block a user