mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 19:03:47 +00:00
refactor: restructure model to contain an object of rundowns
This commit is contained in:
@@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||
|
||||
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||
import { makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import { createBlob, downloadBlob } from './utils';
|
||||
@@ -40,9 +40,11 @@ export async function downloadProject(fileName: string) {
|
||||
export async function downloadCSV(fileName: string = 'rundown') {
|
||||
try {
|
||||
const { data, name } = await fileDownload(fileName);
|
||||
const { project, rundown, customFields } = data;
|
||||
const { project, rundowns, customFields } = data;
|
||||
|
||||
const flatRundowns = aggregateRundowns(rundowns);
|
||||
const sheetData = makeTable(project, flatRundowns, customFields);
|
||||
|
||||
const sheetData = makeTable(project, rundown, customFields);
|
||||
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
||||
|
||||
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { CustomFields, Rundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
const excelPath = `${apiEntryUrl}/excel`;
|
||||
|
||||
type PreviewSpreadsheetResponse = {
|
||||
rundown: OntimeRundown;
|
||||
customFields: CustomFields;
|
||||
};
|
||||
|
||||
/**
|
||||
* upload Excel file to server
|
||||
* @return string - file ID op the uploaded file
|
||||
@@ -34,6 +29,10 @@ export async function getWorksheetNames(): Promise<string[]> {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
type PreviewSpreadsheetResponse = {
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
};
|
||||
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||
options,
|
||||
|
||||
@@ -1,29 +1,44 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
||||
import {
|
||||
MessageResponse,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
ProjectRundownsList,
|
||||
Rundown,
|
||||
TransientEventPayload,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
const rundownPath = `${apiEntryUrl}/rundown`;
|
||||
|
||||
/**
|
||||
* HTTP request to fetch a list of existing rundowns
|
||||
*/
|
||||
export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
||||
const res = await axios.get(`${rundownPath}/`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to fetch all events
|
||||
*/
|
||||
export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
||||
const res = await axios.get(`${rundownPath}/normalised`);
|
||||
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||
const res = await axios.get(`${rundownPath}/current`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to post new event
|
||||
*/
|
||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeEntry>> {
|
||||
return axios.post(rundownPath, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to put new event
|
||||
*/
|
||||
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
export async function requestPutEvent(data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
||||
return axios.put(rundownPath, data);
|
||||
}
|
||||
|
||||
@@ -48,7 +63,7 @@ export type ReorderEntry = {
|
||||
/**
|
||||
* HTTP request to reorder events
|
||||
*/
|
||||
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeEntry>> {
|
||||
return axios.patch(`${rundownPath}/reorder`, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
@@ -54,7 +54,7 @@ export const previewRundown = async (
|
||||
sheetId: string,
|
||||
options: ImportMap,
|
||||
): Promise<{
|
||||
rundown: OntimeRundown;
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
}> => {
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { NormalisedRundown, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||
import { OntimeEntry, Rundown } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
import { fetchNormalisedRundown } from '../api/rundown';
|
||||
import { fetchCurrentRundown } from '../api/rundown';
|
||||
|
||||
import useProjectData from './useProjectData';
|
||||
|
||||
// revision is -1 so that the remote revision is higher
|
||||
const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 };
|
||||
const cachedRundownPlaceholder: Rundown = {
|
||||
id: 'default',
|
||||
title: '',
|
||||
order: [],
|
||||
entries: {},
|
||||
revision: -1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalised rundown data
|
||||
*/
|
||||
export default function useRundown() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||
queryKey: RUNDOWN,
|
||||
queryFn: fetchNormalisedRundown,
|
||||
queryFn: fetchCurrentRundown,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
@@ -37,16 +43,16 @@ export function useFlatRundown() {
|
||||
|
||||
const loadedProject = useRef<string>('');
|
||||
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
||||
const [flatRunDown, setFlatRunDown] = useState<OntimeRundown>([]);
|
||||
const [flatRundown, setFlatRundown] = useState<OntimeEntry[]>([]);
|
||||
|
||||
// update data whenever the revision changes
|
||||
useEffect(() => {
|
||||
if (data.revision !== -1 && data.revision !== prevRevision) {
|
||||
const flatRundown = data.order.map((id) => data.rundown[id]);
|
||||
setFlatRunDown(flatRundown);
|
||||
const flatRundown = data.order.map((id) => data.entries[id]);
|
||||
setFlatRundown(flatRundown);
|
||||
setPrevRevision(data.revision);
|
||||
}
|
||||
}, [data.order, data.revision, data.rundown, prevRevision]);
|
||||
}, [data.entries, data.order, data.revision, prevRevision]);
|
||||
|
||||
// TODO: should we have a project id field?
|
||||
// invalidate current version if project changes
|
||||
@@ -57,13 +63,13 @@ export function useFlatRundown() {
|
||||
}
|
||||
}, [projectData]);
|
||||
|
||||
return { data: flatRunDown, status };
|
||||
return { data: flatRundown, status };
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to a partial rundown based on a filter callback
|
||||
*/
|
||||
export function usePartialRundown(cb: (event: OntimeRundownEntry) => boolean) {
|
||||
export function usePartialRundown(cb: (event: OntimeEntry) => boolean) {
|
||||
const { data, status } = useFlatRundown();
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter(cb);
|
||||
|
||||
@@ -2,11 +2,12 @@ import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
isOntimeEvent,
|
||||
MaybeString,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
OntimeRundownEntry,
|
||||
RundownCached,
|
||||
Rundown,
|
||||
TimeField,
|
||||
TimeStrategy,
|
||||
TransientEventPayload,
|
||||
@@ -31,12 +32,12 @@ import { useEditorSettings } from '../stores/editorSettings';
|
||||
|
||||
export type EventOptions = Partial<{
|
||||
// options to any new block (event / delay / block)
|
||||
after: string;
|
||||
before: string;
|
||||
after: MaybeString;
|
||||
before: MaybeString;
|
||||
// options to blocks of type OntimeEvent
|
||||
defaultPublic: boolean;
|
||||
linkPrevious: boolean;
|
||||
lastEventId: string;
|
||||
lastEventId: MaybeString;
|
||||
}>;
|
||||
|
||||
/**
|
||||
@@ -57,11 +58,11 @@ export const useEventAction = () => {
|
||||
|
||||
const getEventById = useCallback(
|
||||
(eventId: string) => {
|
||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
if (!cachedRundown?.rundown) {
|
||||
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
if (!cachedRundown?.entries) {
|
||||
return;
|
||||
}
|
||||
return cachedRundown.rundown[eventId];
|
||||
return cachedRundown.entries[eventId];
|
||||
},
|
||||
[queryClient],
|
||||
);
|
||||
@@ -100,9 +101,8 @@ export const useEventAction = () => {
|
||||
newEvent.linkStart = applicationOptions.lastEventId;
|
||||
} else if (applicationOptions?.lastEventId) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this is a value
|
||||
const rundownData = queryClient.getQueryData<RundownCached>(RUNDOWN)!;
|
||||
const { rundown } = rundownData;
|
||||
const previousEvent = rundown[applicationOptions.lastEventId];
|
||||
const rundownData = queryClient.getQueryData<Rundown>(RUNDOWN)!;
|
||||
const previousEvent = rundownData.entries[applicationOptions.lastEventId];
|
||||
if (isOntimeEvent(previousEvent)) {
|
||||
newEvent.timeStart = previousEvent.timeEnd;
|
||||
}
|
||||
@@ -180,15 +180,21 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const eventId = newEvent.id;
|
||||
|
||||
if (previousData && eventId) {
|
||||
// optimistically update object
|
||||
const newRundown = { ...previousData.rundown };
|
||||
const newRundown = { ...previousData.entries };
|
||||
// @ts-expect-error -- we expect the events to be of same type
|
||||
newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
|
||||
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: previousData.order,
|
||||
entries: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
}
|
||||
|
||||
// Return a context with the previous and new events
|
||||
@@ -196,7 +202,7 @@ export const useEventAction = () => {
|
||||
},
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _newEvent, context) => {
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -210,7 +216,7 @@ export const useEventAction = () => {
|
||||
* Updates existing event
|
||||
*/
|
||||
const updateEvent = useCallback(
|
||||
async (event: Partial<OntimeRundownEntry>) => {
|
||||
async (event: Partial<OntimeEntry>) => {
|
||||
try {
|
||||
await _updateEventMutation.mutateAsync(event);
|
||||
} catch (error) {
|
||||
@@ -296,9 +302,9 @@ export const useEventAction = () => {
|
||||
* Utility function to get the previous event end time
|
||||
*/
|
||||
function getPreviousEnd(): number {
|
||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
|
||||
if (!cachedRundown?.order || !cachedRundown?.rundown) {
|
||||
if (!cachedRundown?.order || !cachedRundown?.entries) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -308,7 +314,7 @@ export const useEventAction = () => {
|
||||
}
|
||||
let previousEnd = 0;
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const event = cachedRundown.rundown[cachedRundown.order[i]];
|
||||
const event = cachedRundown.entries[cachedRundown.order[i]];
|
||||
if (isOntimeEvent(event)) {
|
||||
previousEnd = event.timeEnd;
|
||||
break;
|
||||
@@ -331,11 +337,11 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const previousRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
|
||||
if (previousEvents) {
|
||||
if (previousRundown) {
|
||||
const eventIds = new Set(ids);
|
||||
const newRundown = { ...previousEvents.rundown };
|
||||
const newRundown = { ...previousRundown.entries };
|
||||
|
||||
eventIds.forEach((eventId) => {
|
||||
if (Object.hasOwn(newRundown, eventId)) {
|
||||
@@ -349,16 +355,22 @@ export const useEventAction = () => {
|
||||
}
|
||||
});
|
||||
|
||||
queryClient.setQueryData(RUNDOWN, { order: previousEvents.order, rundown: newRundown, revision: -1 });
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousRundown.id,
|
||||
title: previousRundown.title,
|
||||
order: previousRundown.order,
|
||||
entries: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
}
|
||||
// Return a context with the previous and new events
|
||||
return { previousEvents };
|
||||
// Return a context with the previous rundown
|
||||
return { previousRundown };
|
||||
},
|
||||
onSettled: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
},
|
||||
onError: (_error, _newEvent, context) => {
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousEvents);
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousRundown);
|
||||
},
|
||||
networkMode: 'always',
|
||||
});
|
||||
@@ -386,19 +398,21 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const newOrder = previousData.order.filter((id) => !eventIds.includes(id));
|
||||
const newRundown = { ...previousData.rundown };
|
||||
const newRundown = { ...previousData.entries };
|
||||
for (const eventId of eventIds) {
|
||||
delete newRundown[eventId];
|
||||
}
|
||||
|
||||
queryClient.setQueryData(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: newOrder,
|
||||
rundown: newRundown,
|
||||
entries: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
}
|
||||
@@ -409,7 +423,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -445,10 +459,16 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(RUNDOWN, { rundown: {}, order: [], revision: -1 });
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData?.id ?? 'default',
|
||||
title: previousData?.title ?? '',
|
||||
entries: {},
|
||||
order: [],
|
||||
revision: -1,
|
||||
});
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousData };
|
||||
@@ -456,7 +476,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -516,13 +536,18 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const newOrder = reorderArray(previousData.order, data.from, data.to);
|
||||
|
||||
queryClient.setQueryData(RUNDOWN, { order: newOrder, rundown: previousData.rundown, revision: -1 });
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: newOrder,
|
||||
entries: previousData.entries,
|
||||
revision: -1,
|
||||
});
|
||||
}
|
||||
|
||||
// Return a context with the previous and new events
|
||||
@@ -531,7 +556,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -572,22 +597,28 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const newRundown = { ...previousData.rundown };
|
||||
const eventA = previousData.rundown[from];
|
||||
const eventB = previousData.rundown[to];
|
||||
const newRundown = { ...previousData.entries };
|
||||
const eventA = previousData.entries[from];
|
||||
const eventB = previousData.entries[to];
|
||||
|
||||
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { newA, newB } = swapEventData(eventA, eventB);
|
||||
const [newA, newB] = swapEventData(eventA, eventB);
|
||||
newRundown[from] = newA;
|
||||
newRundown[to] = newB;
|
||||
|
||||
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: previousData.order,
|
||||
entries: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
}
|
||||
|
||||
// Return a context with the previous events
|
||||
@@ -596,7 +627,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { makeCSVFromArrayOfArrays } from '../csv';
|
||||
import { OntimeEntry, ProjectRundowns, Rundown } from 'ontime-types';
|
||||
|
||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../csv';
|
||||
|
||||
describe('makeCSVFromArrayOfArrays()', () => {
|
||||
it('joins an array of arrays with commas and newlines', () => {
|
||||
@@ -11,3 +13,32 @@ after newline,after comma
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateRundowns()', () => {
|
||||
it('flattens an object of rundowns into a single array', () => {
|
||||
const rundowns = {
|
||||
first: {
|
||||
id: '',
|
||||
title: '',
|
||||
revision: 0,
|
||||
order: ['1', '2'],
|
||||
entries: {
|
||||
'1': { id: '1' } as OntimeEntry,
|
||||
'2': { id: '2' } as OntimeEntry,
|
||||
},
|
||||
},
|
||||
second: {
|
||||
id: '',
|
||||
title: '',
|
||||
revision: 0,
|
||||
order: ['3', '4'],
|
||||
entries: {
|
||||
'3': { id: '3' } as OntimeEntry,
|
||||
'4': { id: '4' } as OntimeEntry,
|
||||
},
|
||||
} as Rundown,
|
||||
} as ProjectRundowns;
|
||||
|
||||
expect(aggregateRundowns(rundowns)).toStrictEqual([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EndAction, EventCustomFields, OntimeEvent, SupportedEvent, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEvent, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { cloneEvent } from '../eventsManager';
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('cloneEvent()', () => {
|
||||
gap: 0,
|
||||
custom: {
|
||||
lighting: '3',
|
||||
} as EventCustomFields,
|
||||
} as EntryCustomFields,
|
||||
};
|
||||
|
||||
const cloned = cloneEvent(original);
|
||||
|
||||
@@ -64,13 +64,13 @@ describe('getRouteFromPreset()', () => {
|
||||
describe('handle url sharing edge cases', () => {
|
||||
it('finds the correct preset when the url contains extra arguments', () => {
|
||||
const location = resolvePath('/demopage?locked=true&token=123');
|
||||
expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy()
|
||||
})
|
||||
expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('appends the feature params to the alias', () => {
|
||||
const location = resolvePath('/demopage?locked=true&token=123');
|
||||
expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123')
|
||||
})
|
||||
expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,25 +83,26 @@ describe('generatePathFromPreset()', () => {
|
||||
});
|
||||
|
||||
test('appends the feature params to the alias', () => {
|
||||
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe('timer?user=guest&alias=demopage&locked=true&token=123');
|
||||
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe(
|
||||
'timer?user=guest&alias=demopage&locked=true&token=123',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arePathsEquivalent()', () => {
|
||||
it("checks whether the paths match", () => {
|
||||
it('checks whether the paths match', () => {
|
||||
expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer', 'timer')).toBeTruthy();
|
||||
expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
it("checks whether the params match", () => {
|
||||
it('checks whether the params match', () => {
|
||||
expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
it("considers edge cases for the url sharing feature", () => {
|
||||
it('considers edge cases for the url sharing feature', () => {
|
||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||
import { OntimeEntry, ProjectRundowns } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* @description Converts an array of arrays to a CSV file
|
||||
* @param {string[][]} arrayOfArrays
|
||||
* @return {string}
|
||||
* Converts an array of arrays to a CSV file
|
||||
*/
|
||||
export function makeCSVFromArrayOfArrays(arrayOfArrays: string[][]): string {
|
||||
return stringify(arrayOfArrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives an object of rundowns, and flattens them into a single, linear rundown
|
||||
* Used for CSV export
|
||||
*/
|
||||
export function aggregateRundowns(rundowns: ProjectRundowns): OntimeEntry[] {
|
||||
const rundownKeys = Object.keys(rundowns);
|
||||
if (rundownKeys.length === 0) return [];
|
||||
const flatRundown: OntimeEntry[] = [];
|
||||
|
||||
for (const key of rundownKeys) {
|
||||
const { order, entries } = rundowns[key];
|
||||
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
const entryId = order[i];
|
||||
const entry = entries[entryId];
|
||||
|
||||
flatRundown.push(entry);
|
||||
}
|
||||
}
|
||||
return flatRundown;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
||||
isPublic: event.isPublic,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
currentBlock: event.currentBlock,
|
||||
revision: 0,
|
||||
delay: event.delay, // the events will be collocated, so having the same metadata is a good start
|
||||
dayOffset: event.dayOffset,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Log, RundownCached, RuntimeStore } from 'ontime-types';
|
||||
import { Log, Rundown, RuntimeStore } from 'ontime-types';
|
||||
|
||||
import { isProduction, websocketUrl } from '../../externals';
|
||||
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants';
|
||||
@@ -203,7 +203,7 @@ export const connectSocket = () => {
|
||||
invalidateAllCaches();
|
||||
} else if (target === 'RUNDOWN') {
|
||||
const { revision } = payload;
|
||||
const currentRevision = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN)?.revision ?? -1;
|
||||
const currentRevision = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN)?.revision ?? -1;
|
||||
if (revision > currentRevision) {
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ export default function ReportSettings() {
|
||||
};
|
||||
|
||||
const combinedReport = useMemo(() => {
|
||||
return getCombinedReport(reportData, data.rundown, data.order);
|
||||
}, [reportData, data.rundown, data.order]);
|
||||
return getCombinedReport(reportData, data.entries, data.order);
|
||||
}, [reportData, data.entries, data.order]);
|
||||
|
||||
return (
|
||||
<Panel.Section>
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { isOntimeEvent, MaybeNumber, NormalisedRundown, OntimeReport } from 'ontime-types';
|
||||
import { EntryId, isOntimeEvent, MaybeNumber, OntimeReport, RundownEntries } from 'ontime-types';
|
||||
|
||||
import { makeCSVFromArrayOfArrays } from '../../../../common/utils/csv';
|
||||
import { formatTime } from '../../../../common/utils/time';
|
||||
@@ -16,7 +16,7 @@ export type CombinedReport = {
|
||||
/**
|
||||
* Creates a combined report with the rundown data
|
||||
*/
|
||||
export function getCombinedReport(report: OntimeReport, rundown: NormalisedRundown, order: string[]): CombinedReport[] {
|
||||
export function getCombinedReport(report: OntimeReport, rundown: RundownEntries, order: EntryId[]): CombinedReport[] {
|
||||
if (Object.keys(report).length === 0) return [];
|
||||
if (order.length === 0) return [];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export async function makeProjectPatch(data: DatabaseModel, mergeKeys: Record<st
|
||||
for (const key in mergeKeys) {
|
||||
if (isKeyOfType(key, data) && mergeKeys[key]) {
|
||||
// if the rundown is merged we also need the custom fields
|
||||
if (key === 'rundown') {
|
||||
if (key === 'rundowns') {
|
||||
patchObject.customFields = data['customFields'];
|
||||
}
|
||||
Object.assign(patchObject, { [key]: data[key] });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { CustomFields, Rundown } from 'ontime-types';
|
||||
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
@@ -9,7 +9,7 @@ import useGoogleSheet from './useGoogleSheet';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
interface ImportReviewProps {
|
||||
rundown: OntimeRundown;
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
onFinished: () => void;
|
||||
onCancel: () => void;
|
||||
@@ -29,7 +29,12 @@ export default function ImportReview(props: ImportReviewProps) {
|
||||
|
||||
const applyImport = async () => {
|
||||
setLoading(true);
|
||||
await importRundown(rundown, customFields);
|
||||
await importRundown(
|
||||
{
|
||||
[rundown.id]: rundown,
|
||||
},
|
||||
customFields,
|
||||
);
|
||||
setLoading(false);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
+32
-31
@@ -1,6 +1,6 @@
|
||||
import { Fragment } from 'react';
|
||||
import { IoLink } from 'react-icons/io5';
|
||||
import { CustomFields, isOntimeBlock, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
import { CustomFields, isOntimeBlock, isOntimeEvent, Rundown } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import Tag from '../../../../../common/components/tag/Tag';
|
||||
@@ -10,7 +10,7 @@ import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
import style from './PreviewRundown.module.scss';
|
||||
|
||||
interface PreviewRundownProps {
|
||||
rundown: OntimeRundown;
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
}
|
||||
|
||||
@@ -53,75 +53,76 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rundown.map((event) => {
|
||||
if (isOntimeBlock(event)) {
|
||||
{rundown.order.map((entryId) => {
|
||||
const entry = rundown.entries[entryId];
|
||||
if (isOntimeBlock(entry)) {
|
||||
return (
|
||||
<tr key={event.id}>
|
||||
<tr key={entry.id}>
|
||||
<td className={style.center}>
|
||||
<Tag>-</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.type}</Tag>
|
||||
<Tag>{entry.type}</Tag>
|
||||
</td>
|
||||
<td />
|
||||
<td colSpan={99}>{event.title}</td>
|
||||
<td colSpan={99}>{entry.title}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
if (!isOntimeEvent(event)) {
|
||||
if (!isOntimeEvent(entry)) {
|
||||
return null;
|
||||
}
|
||||
eventIndex += 1;
|
||||
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
||||
const countToEnd = booleanToText(event.countToEnd);
|
||||
const isPublic = booleanToText(event.isPublic);
|
||||
const skip = booleanToText(event.skip);
|
||||
const colour = entry.colour ? getAccessibleColour(entry.colour) : {};
|
||||
const countToEnd = booleanToText(entry.countToEnd);
|
||||
const isPublic = booleanToText(entry.isPublic);
|
||||
const skip = booleanToText(entry.skip);
|
||||
|
||||
return (
|
||||
<Fragment key={event.id}>
|
||||
<Fragment key={entry.id}>
|
||||
<tr>
|
||||
<td className={style.center}>
|
||||
<Tag>{eventIndex}</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.type}</Tag>
|
||||
<Tag>{entry.type}</Tag>
|
||||
</td>
|
||||
<td className={style.nowrap}>{event.cue}</td>
|
||||
<td>{event.title}</td>
|
||||
<td className={style.nowrap}>{entry.cue}</td>
|
||||
<td>{entry.title}</td>
|
||||
<td className={style.flex}>
|
||||
<span className={event.linkStart ? style.subdued : undefined}>{millisToString(event.timeStart)}</span>
|
||||
{event.linkStart && <IoLink className={style.linkStartActive} />}
|
||||
<span className={entry.linkStart ? style.subdued : undefined}>{millisToString(entry.timeStart)}</span>
|
||||
{entry.linkStart && <IoLink className={style.linkStartActive} />}
|
||||
</td>
|
||||
<td>{millisToString(event.timeEnd)}</td>
|
||||
<td>{millisToString(event.duration)}</td>
|
||||
<td>{millisToString(event.timeWarning)}</td>
|
||||
<td>{millisToString(event.timeDanger)}</td>
|
||||
<td>{millisToString(entry.timeEnd)}</td>
|
||||
<td>{millisToString(entry.duration)}</td>
|
||||
<td>{millisToString(entry.timeWarning)}</td>
|
||||
<td>{millisToString(entry.timeDanger)}</td>
|
||||
<td className={style.center}>{countToEnd && <Tag>{countToEnd}</Tag>}</td>
|
||||
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
||||
<td>{skip && <Tag>{skip}</Tag>}</td>
|
||||
<td style={{ ...colour }}>{event.colour}</td>
|
||||
<td style={{ ...colour }}>{entry.colour}</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.timerType}</Tag>
|
||||
<Tag>{entry.timerType}</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.endAction}</Tag>
|
||||
<Tag>{entry.endAction}</Tag>
|
||||
</td>
|
||||
{isOntimeEvent(event) &&
|
||||
{isOntimeEvent(entry) &&
|
||||
fieldKeys.map((field) => {
|
||||
let value = '';
|
||||
if (field in event.custom) {
|
||||
value = event.custom[field];
|
||||
if (field in entry.custom) {
|
||||
value = entry.custom[field];
|
||||
}
|
||||
return <td key={field}>{value}</td>;
|
||||
})}
|
||||
<td className={style.center}>
|
||||
<Tag>{event.id}</Tag>
|
||||
<Tag>{entry.id}</Tag>
|
||||
</td>
|
||||
</tr>
|
||||
{event.note && (
|
||||
{entry.note && (
|
||||
<tr>
|
||||
<td colSpan={99} className={style.secondaryRow}>
|
||||
Note: {event.note}
|
||||
Note: {entry.note}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, ProjectRundowns } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
|
||||
@@ -75,9 +75,9 @@ export default function useGoogleSheet() {
|
||||
};
|
||||
|
||||
/** applies rundown and customFields to current project */
|
||||
const importRundown = async (rundown: OntimeRundown, customFields: CustomFields) => {
|
||||
const importRundown = async (rundowns: ProjectRundowns, customFields: CustomFields) => {
|
||||
try {
|
||||
await patchData({ rundown, customFields });
|
||||
await patchData({ rundowns, customFields });
|
||||
// we are unable to optimistically set the rundown since we need
|
||||
// it to be normalised
|
||||
await queryClient.invalidateQueries({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||
import { defaultImportMap, ImportMap } from 'ontime-utils';
|
||||
import { create } from 'zustand';
|
||||
|
||||
@@ -15,8 +15,8 @@ type SheetStore = {
|
||||
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
||||
|
||||
// we get this from a preview response
|
||||
rundown: OntimeRundown | null;
|
||||
setRundown: (rundown: OntimeRundown | null) => void;
|
||||
rundown: Rundown | null;
|
||||
setRundown: (rundown: Rundown | null) => void;
|
||||
|
||||
// we get this from a preview response
|
||||
customFields: CustomFields | null;
|
||||
@@ -60,7 +60,7 @@ export const useSheetStore = create<SheetStore>((set, get) => ({
|
||||
|
||||
setAuthenticationStatus: (status: AuthenticationStatus) => set({ authenticationStatus: status }),
|
||||
|
||||
setRundown: (rundown: OntimeRundown | null) => set({ rundown }),
|
||||
setRundown: (rundown: Rundown | null) => set({ rundown }),
|
||||
|
||||
setCustomFields: (customFields: CustomFields | null) => set({ customFields }),
|
||||
|
||||
|
||||
@@ -126,8 +126,8 @@ export default function Operator() {
|
||||
let isPast = Boolean(featureData.selectedEventId);
|
||||
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||
|
||||
const { firstEvent } = getFirstEventNormal(data.rundown, data.order);
|
||||
const { lastEvent } = getLastEventNormal(data.rundown, data.order);
|
||||
const { firstEvent } = getFirstEventNormal(data.entries, data.order);
|
||||
const { lastEvent } = getLastEventNormal(data.entries, data.order);
|
||||
|
||||
return (
|
||||
<div className={style.operatorContainer}>
|
||||
@@ -152,7 +152,7 @@ export default function Operator() {
|
||||
|
||||
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
||||
{data.order.map((eventId) => {
|
||||
const entry = data.rundown[eventId];
|
||||
const entry = data.entries[eventId];
|
||||
if (isOntimeEvent(entry)) {
|
||||
const isSelected = featureData.selectedEventId === entry.id;
|
||||
if (isSelected) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import {
|
||||
type EntryId,
|
||||
type MaybeString,
|
||||
type PlayableEvent,
|
||||
type Rundown,
|
||||
isOntimeBlock,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
MaybeString,
|
||||
PlayableEvent,
|
||||
Playback,
|
||||
RundownCached,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
getPreviousBlockNormal,
|
||||
getPreviousNormal,
|
||||
isNewLatest,
|
||||
reorderArray,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { type EventOptions, useEventAction } from '../../common/hooks/useEventAction';
|
||||
@@ -39,12 +41,12 @@ import style from './Rundown.module.scss';
|
||||
const RundownEntry = lazy(() => import('./RundownEntry'));
|
||||
|
||||
interface RundownProps {
|
||||
data: RundownCached;
|
||||
data: Rundown;
|
||||
}
|
||||
|
||||
export default function Rundown({ data }: RundownProps) {
|
||||
const { order, rundown } = data;
|
||||
const [statefulEntries, setStatefulEntries] = useState(order);
|
||||
const { order, entries } = data;
|
||||
const [statefulEntries, setStatefulEntries] = useState<EntryId[]>(order);
|
||||
|
||||
const featureData = useRundownEditor();
|
||||
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
||||
@@ -65,30 +67,30 @@ export default function Rundown({ data }: RundownProps) {
|
||||
const deleteAtCursor = useCallback(
|
||||
(cursor: string | null) => {
|
||||
if (!cursor) return;
|
||||
const { entry, index } = getPreviousNormal(rundown, order, cursor);
|
||||
const { entry, index } = getPreviousNormal(entries, order, cursor);
|
||||
deleteEvent([cursor]);
|
||||
if (entry && index !== null) {
|
||||
setSelectedEvents({ id: entry.id, selectMode: 'click', index });
|
||||
}
|
||||
},
|
||||
[rundown, order, deleteEvent, setSelectedEvents],
|
||||
[entries, order, deleteEvent, setSelectedEvents],
|
||||
);
|
||||
|
||||
const insertCopyAtId = useCallback(
|
||||
(atId: string | null, copyId: string | null, above = false) => {
|
||||
const adjustedCursor = above ? getPreviousNormal(rundown, order, atId ?? '').entry?.id ?? null : atId;
|
||||
const adjustedCursor = above ? getPreviousNormal(entries, order, atId ?? '').entry?.id ?? null : atId;
|
||||
if (copyId === null) {
|
||||
// we cant clone without selection
|
||||
return;
|
||||
}
|
||||
const cloneEntry = rundown[copyId];
|
||||
const cloneEntry = entries[copyId];
|
||||
if (cloneEntry?.type === SupportedEvent.Event) {
|
||||
//if we don't have a cursor add the new event on top
|
||||
const newEvent = cloneEvent(cloneEntry);
|
||||
addEvent(newEvent, { after: adjustedCursor ?? undefined });
|
||||
}
|
||||
},
|
||||
[addEvent, order, rundown],
|
||||
[addEvent, order, entries],
|
||||
);
|
||||
|
||||
const insertAtId = useCallback(
|
||||
@@ -124,7 +126,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
let newCursor = cursor;
|
||||
if (cursor === null) {
|
||||
// there is no cursor, we select the first or last depending on direction
|
||||
const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
|
||||
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||
|
||||
if (isOntimeBlock(selected)) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
@@ -140,14 +142,14 @@ export default function Rundown({ data }: RundownProps) {
|
||||
// otherwise we select the next or previous
|
||||
const selected =
|
||||
direction === 'up'
|
||||
? getPreviousBlockNormal(rundown, order, newCursor)
|
||||
: getNextBlockNormal(rundown, order, newCursor);
|
||||
? getPreviousBlockNormal(entries, order, newCursor)
|
||||
: getNextBlockNormal(entries, order, newCursor);
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
}
|
||||
},
|
||||
[order, rundown, setSelectedEvents],
|
||||
[order, entries, setSelectedEvents],
|
||||
);
|
||||
|
||||
const selectEntry = useCallback(
|
||||
@@ -158,7 +160,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
|
||||
if (cursor === null) {
|
||||
// there is no cursor, we select the first or last depending on direction if it exists
|
||||
const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
|
||||
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||
if (selected !== null) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
}
|
||||
@@ -167,13 +169,13 @@ export default function Rundown({ data }: RundownProps) {
|
||||
|
||||
// otherwise we select the next or previous
|
||||
const selected =
|
||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
||||
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
}
|
||||
},
|
||||
[order, rundown, setSelectedEvents],
|
||||
[order, entries, setSelectedEvents],
|
||||
);
|
||||
|
||||
const moveEntry = useCallback(
|
||||
@@ -182,14 +184,14 @@ export default function Rundown({ data }: RundownProps) {
|
||||
return;
|
||||
}
|
||||
const { index } =
|
||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
||||
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
|
||||
|
||||
if (index !== null) {
|
||||
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
||||
reorderEvent(cursor, offsetIndex, index);
|
||||
}
|
||||
},
|
||||
[order, reorderEvent, rundown],
|
||||
[order, reorderEvent, entries],
|
||||
);
|
||||
|
||||
// shortcuts
|
||||
@@ -238,6 +240,9 @@ export default function Rundown({ data }: RundownProps) {
|
||||
setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index });
|
||||
}, [appMode, featureData.selectedEventId, order, setSelectedEvents]);
|
||||
|
||||
/**
|
||||
* On drag end, we reorder the events
|
||||
*/
|
||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
@@ -245,9 +250,10 @@ export default function Rundown({ data }: RundownProps) {
|
||||
if (active.id !== over?.id) {
|
||||
const fromIndex = active.data.current?.sortable.index;
|
||||
const toIndex = over.data.current?.sortable.index;
|
||||
// ugly hack to handle inconsistencies between dnd-kit and async store updates
|
||||
|
||||
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
||||
setStatefulEntries((currentEntries) => {
|
||||
return arrayMove(currentEntries, fromIndex, toIndex);
|
||||
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||
});
|
||||
reorderEvent(String(active.id), fromIndex, toIndex);
|
||||
}
|
||||
@@ -259,11 +265,11 @@ export default function Rundown({ data }: RundownProps) {
|
||||
}
|
||||
|
||||
// last event is used to calculate relative timings
|
||||
let lastEvent: PlayableEvent | undefined; // used by indicators
|
||||
let thisEvent: PlayableEvent | undefined;
|
||||
let lastEvent: PlayableEvent | null = null; // used by indicators
|
||||
let thisEvent: PlayableEvent | null = null;
|
||||
// previous entry is used to infer position in the rundown for new events
|
||||
let previousEntryId: string | undefined;
|
||||
let thisId = previousEntryId;
|
||||
let previousEntryId: MaybeString = null;
|
||||
let thisId: MaybeString = null;
|
||||
|
||||
let eventIndex = 0;
|
||||
// all events before the current selected are in the past
|
||||
@@ -272,6 +278,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
let totalGap = 0;
|
||||
const isEditMode = appMode === AppMode.Edit;
|
||||
let isLinkedToLoaded = true; //check if the event can link all the way back to the currently playing event
|
||||
|
||||
return (
|
||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
||||
@@ -281,7 +288,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
// we iterate through a stateful copy of order to make the operations smoother
|
||||
// this means that this can be out of sync with order until the useEffect runs
|
||||
// instead of writing all the logic guards, we simply short circuit rendering here
|
||||
const entry = rundown[entryId];
|
||||
const entry = entries[entryId];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useCallback } from 'react';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||
import {
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
MaybeString,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||
@@ -28,13 +37,13 @@ export type EventItemActions =
|
||||
interface RundownEntryProps {
|
||||
type: SupportedEvent;
|
||||
isPast: boolean;
|
||||
data: OntimeRundownEntry;
|
||||
data: OntimeEntry;
|
||||
loaded: boolean;
|
||||
eventIndex: number;
|
||||
hasCursor: boolean;
|
||||
isNext: boolean;
|
||||
isNextDay: boolean;
|
||||
previousEntryId?: string;
|
||||
previousEntryId: MaybeString;
|
||||
previousEventId?: string;
|
||||
playback?: Playback; // we only care about this if this event is playing
|
||||
isRolling: boolean; // we need to know even if not related to this event
|
||||
@@ -150,7 +159,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
}
|
||||
});
|
||||
|
||||
if (data.type === SupportedEvent.Event) {
|
||||
if (isOntimeEvent(data)) {
|
||||
return (
|
||||
<EventBlock
|
||||
eventId={data.id}
|
||||
@@ -167,7 +176,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
timerType={data.timerType}
|
||||
title={data.title}
|
||||
note={data.note}
|
||||
delay={data.delay ?? 0}
|
||||
delay={data.delay}
|
||||
colour={data.colour}
|
||||
isPast={isPast}
|
||||
isNext={isNext}
|
||||
@@ -184,9 +193,15 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
);
|
||||
} else if (data.type === SupportedEvent.Block) {
|
||||
return <BlockBlock data={data} hasCursor={hasCursor} onDelete={() => actionHandler('delete')} />;
|
||||
} else if (data.type === SupportedEvent.Delay) {
|
||||
} else if (isOntimeBlock(data)) {
|
||||
return (
|
||||
<BlockBlock data={data} hasCursor={hasCursor}>
|
||||
{data.events.map((eventId) => {
|
||||
return <div key={eventId}>{eventId}</div>;
|
||||
})}
|
||||
</BlockBlock>
|
||||
);
|
||||
} else if (isOntimeDelay(data)) {
|
||||
return <DelayBlock data={data} hasCursor={hasCursor} />;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -22,7 +22,3 @@
|
||||
.drag {
|
||||
@include drag-style;
|
||||
}
|
||||
|
||||
.actionMenu {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef } from 'react';
|
||||
import { PropsWithChildren, useRef } from 'react';
|
||||
import { IoReorderTwo } from 'react-icons/io5';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
@@ -7,18 +7,15 @@ import { OntimeBlock } from 'ontime-types';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||
|
||||
import BlockDelete from './BlockDelete';
|
||||
|
||||
import style from './BlockBlock.module.scss';
|
||||
|
||||
interface BlockBlockProps {
|
||||
data: OntimeBlock;
|
||||
hasCursor: boolean;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export default function BlockBlock(props: BlockBlockProps) {
|
||||
const { data, hasCursor, onDelete } = props;
|
||||
export default function BlockBlock(props: PropsWithChildren<BlockBlockProps>) {
|
||||
const { data, hasCursor, children } = props;
|
||||
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
@@ -46,7 +43,8 @@ export default function BlockBlock(props: BlockBlockProps) {
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
||||
<BlockDelete onDelete={onDelete} />
|
||||
<button>+++</button>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { IoTrash } from 'react-icons/io5';
|
||||
import { IconButton } from '@chakra-ui/react';
|
||||
|
||||
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';
|
||||
|
||||
interface BlockDeleteProps {
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export default function BlockDelete(props: BlockDeleteProps) {
|
||||
const { onDelete } = props;
|
||||
const mode = useAppMode((state) => state.mode);
|
||||
|
||||
const isRunMode = mode === AppMode.Run;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label='Delete'
|
||||
size='sm'
|
||||
icon={<IoTrash />}
|
||||
variant='ontime-subtle'
|
||||
color='#FA5656'
|
||||
onClick={onDelete}
|
||||
isDisabled={isRunMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -15,23 +15,22 @@ interface CuesheetEventEditorProps {
|
||||
export default function CuesheetEventEditor(props: CuesheetEventEditorProps) {
|
||||
const { eventId } = props;
|
||||
const { data } = useRundown();
|
||||
const { order, rundown } = data;
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (order.length === 0) {
|
||||
if (data.order.length === 0) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = rundown[eventId];
|
||||
const event = data.entries[eventId];
|
||||
if (event && isOntimeEvent(event)) {
|
||||
setEvent(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
}
|
||||
}, [data, eventId, order, rundown]);
|
||||
}, [eventId, data.order, data.entries]);
|
||||
|
||||
if (!event) {
|
||||
return null;
|
||||
|
||||
@@ -13,29 +13,28 @@ import style from './EventEditor.module.scss';
|
||||
export default function RundownEventEditor() {
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const { data } = useRundown();
|
||||
const { order, rundown } = data;
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (order.length === 0) {
|
||||
if (data.order.length === 0) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
|
||||
const selectedEventId = data.order.find((entryId) => selectedEvents.has(entryId));
|
||||
if (!selectedEventId) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
const event = rundown[selectedEventId];
|
||||
const event = data.entries[selectedEventId];
|
||||
|
||||
if (event && isOntimeEvent(event)) {
|
||||
setEvent(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
}
|
||||
}, [order, rundown, selectedEvents]);
|
||||
}, [data.order, data.entries, selectedEvents]);
|
||||
|
||||
if (!event) {
|
||||
return <EventEditorEmpty />;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
import { MaybeString, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
@@ -9,7 +9,7 @@ import { useEmitLog } from '../../../common/stores/logger';
|
||||
import style from './QuickAddBlock.module.scss';
|
||||
|
||||
interface QuickAddBlockProps {
|
||||
previousEventId?: string;
|
||||
previousEventId: MaybeString;
|
||||
}
|
||||
|
||||
export default memo(QuickAddBlock);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { isOntimeEvent, MaybeNumber, MaybeString, OntimeEvent, RundownCached } from 'ontime-types';
|
||||
import { isOntimeEvent, MaybeNumber, MaybeString, OntimeEvent, Rundown } from 'ontime-types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { RUNDOWN } from '../../common/api/constants';
|
||||
@@ -33,7 +33,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
|
||||
// on ctrl + click, we toggle the selection of that event
|
||||
if (selectMode === 'ctrl') {
|
||||
const rundownData = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
if (!rundownData) return;
|
||||
|
||||
// if it doesnt exist, simply add to the list and set an anchor
|
||||
@@ -50,7 +50,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
selectedEvents.delete(id);
|
||||
|
||||
const nextIndex = rundownData.order.findIndex(
|
||||
(eventId, i) => i > index && isOntimeEvent(rundownData.rundown[eventId]) && selectedEvents.has(eventId),
|
||||
(eventId, i) => i > index && isOntimeEvent(rundownData.entries[eventId]) && selectedEvents.has(eventId),
|
||||
);
|
||||
|
||||
// if we didnt find anything after, set the anchor to the last event
|
||||
@@ -62,13 +62,13 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
|
||||
// on shift + click, we select a range of events up to the clicked event
|
||||
if (selectMode === 'shift') {
|
||||
const rundownData = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
if (!rundownData) return;
|
||||
|
||||
// get list of rundown with only ontime events
|
||||
const events: OntimeEvent[] = [];
|
||||
rundownData.order.forEach((eventId) => {
|
||||
const event = rundownData.rundown[eventId];
|
||||
const event = rundownData.entries[eventId];
|
||||
if (isOntimeEvent(event)) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
OntimeRundownEntry,
|
||||
Playback,
|
||||
ProjectData,
|
||||
Runtime,
|
||||
@@ -72,7 +72,7 @@ export default function Countdown(props: CountdownProps) {
|
||||
}
|
||||
if (followThis !== null) {
|
||||
setFollow(followThis);
|
||||
const idx: number = backstageEvents.findIndex((event: OntimeRundownEntry) => event.id === followThis?.id);
|
||||
const idx: number = backstageEvents.findIndex((event: OntimeEntry) => event.id === followThis?.id);
|
||||
const delayToEvent = backstageEvents[idx]?.delay ?? 0;
|
||||
setDelay(delayToEvent);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
||||
import { OntimeEntry, OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import Empty from '../../../common/components/state/Empty';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
@@ -10,7 +10,7 @@ import { sanitiseTitle } from './countdown.helpers';
|
||||
import './Countdown.scss';
|
||||
|
||||
interface CountdownSelectProps {
|
||||
events: OntimeRundownEntry[];
|
||||
events: OntimeEntry[];
|
||||
}
|
||||
|
||||
const scheduleFormat = { format12: 'hh:mm a', format24: 'HH:mm' };
|
||||
@@ -19,9 +19,7 @@ export default function CountdownSelect(props: CountdownSelectProps) {
|
||||
const { events } = props;
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
const filteredEvents = events.filter(
|
||||
(event: OntimeRundownEntry) => event.type === SupportedEvent.Event,
|
||||
) as OntimeEvent[];
|
||||
const filteredEvents = events.filter((event: OntimeEntry) => event.type === SupportedEvent.Event) as OntimeEvent[];
|
||||
|
||||
return (
|
||||
<div className='event-select' data-testid='countdown__select'>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type { MaybeString, OntimeEvent, OntimeRundown, ProjectData, Settings } from 'ontime-types';
|
||||
import type { MaybeString, OntimeEntry, OntimeEvent, ProjectData, Settings } from 'ontime-types';
|
||||
import { Playback } from 'ontime-types';
|
||||
import { millisToString, removeSeconds, secondsInMillis } from 'ontime-utils';
|
||||
|
||||
@@ -17,7 +17,7 @@ import StudioClockSchedule from './StudioClockSchedule';
|
||||
import './StudioClock.scss';
|
||||
|
||||
interface StudioClockProps {
|
||||
backstageEvents: OntimeRundown;
|
||||
backstageEvents: OntimeEntry[];
|
||||
eventNext: OntimeEvent | null;
|
||||
general: ProjectData;
|
||||
isMirrored: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
import { isOntimeEvent, MaybeString, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
@@ -8,7 +8,7 @@ import { trimRundown } from './studioClock.utils';
|
||||
import './StudioClock.scss';
|
||||
|
||||
interface StudioClockScheduleProps {
|
||||
rundown: OntimeRundown;
|
||||
rundown: OntimeEntry[];
|
||||
selectedId: MaybeString;
|
||||
nextId: MaybeString;
|
||||
onAir: boolean;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||
import { isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ScheduleProvider = ({
|
||||
isBackstage = false,
|
||||
}: PropsWithChildren<ScheduleProviderProps>) => {
|
||||
const { cycleInterval, stopCycle } = useScheduleOptions();
|
||||
const { data: events } = usePartialRundown((event: OntimeRundownEntry) => {
|
||||
const { data: events } = usePartialRundown((event: OntimeEntry) => {
|
||||
if (isBackstage) {
|
||||
return isOntimeEvent(event);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import useColumnManager from '../cuesheet-table/useColumnManager';
|
||||
|
||||
interface CuesheetDndProps {
|
||||
columns: ColumnDef<OntimeRundownEntry>[];
|
||||
columns: ColumnDef<OntimeEntry>[];
|
||||
}
|
||||
|
||||
export default function CuesheetDnd(props: PropsWithChildren<CuesheetDndProps>) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useTableNav } from '@table-nav/react';
|
||||
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types';
|
||||
import { isOntimeEvent, MaybeString, OntimeEntry, OntimeEvent, TimeField } from 'ontime-types';
|
||||
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
||||
@@ -16,8 +16,8 @@ import useColumnManager from './useColumnManager';
|
||||
import style from './CuesheetTable.module.scss';
|
||||
|
||||
interface CuesheetTableProps {
|
||||
data: OntimeRundown;
|
||||
columns: ColumnDef<OntimeRundownEntry>[];
|
||||
data: OntimeEntry[];
|
||||
columns: ColumnDef<OntimeEntry>[];
|
||||
showModal: (eventId: MaybeString) => void;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
import { RowModel, Table } from '@tanstack/react-table';
|
||||
import Color from 'color';
|
||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeEntry } from 'ontime-types';
|
||||
|
||||
import { useSelectedEventId } from '../../../../common/hooks/useSocket';
|
||||
import { lazyEvaluate } from '../../../../common/utils/lazyEvaluate';
|
||||
@@ -13,9 +13,9 @@ import DelayRow from './DelayRow';
|
||||
import EventRow from './EventRow';
|
||||
|
||||
interface CuesheetBodyProps {
|
||||
rowModel: RowModel<OntimeRundownEntry>;
|
||||
rowModel: RowModel<OntimeEntry>;
|
||||
selectedRef: MutableRefObject<HTMLTableRowElement | null>;
|
||||
table: Table<OntimeRundownEntry>;
|
||||
table: Table<OntimeEntry>;
|
||||
}
|
||||
|
||||
export default function CuesheetBody(props: CuesheetBodyProps) {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { useCuesheetOptions } from '../../cuesheet.options';
|
||||
@@ -10,7 +10,7 @@ import { SortableCell } from './SortableCell';
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
|
||||
interface CuesheetHeaderProps {
|
||||
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
||||
headerGroups: HeaderGroup<OntimeEntry>[];
|
||||
}
|
||||
|
||||
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { memo, MutableRefObject, useLayoutEffect, useRef, useState } from 'react
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { flexRender, Table } from '@tanstack/react-table';
|
||||
import Color from 'color';
|
||||
import { OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
@@ -21,7 +21,7 @@ interface EventRowProps {
|
||||
skip?: boolean;
|
||||
colour?: string;
|
||||
rowBgColour?: string;
|
||||
table: Table<OntimeRundownEntry>;
|
||||
table: Table<OntimeEntry>;
|
||||
/** hack to force re-rendering of the row when the column sizes change */
|
||||
columnHash: string;
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,12 +2,12 @@ import { CSSProperties, ReactNode } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Header } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import styles from '../CuesheetTable.module.scss';
|
||||
|
||||
interface SortableCellProps {
|
||||
header: Header<OntimeRundownEntry, unknown>;
|
||||
header: Header<OntimeEntry, unknown>;
|
||||
style: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry, TimeStrategy } from 'ontime-types';
|
||||
import { CustomFields, isOntimeEvent, OntimeEntry, OntimeEvent, TimeStrategy } from 'ontime-types';
|
||||
import { millisToString, removeSeconds } from 'ontime-utils';
|
||||
|
||||
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
||||
@@ -11,7 +11,7 @@ import MultiLineCell from './MultiLineCell';
|
||||
import SingleLineCell from './SingleLineCell';
|
||||
import TimeInput from './TimeInput';
|
||||
|
||||
function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function MakeStart({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unk
|
||||
);
|
||||
}
|
||||
|
||||
function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function MakeEnd({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unkno
|
||||
);
|
||||
}
|
||||
|
||||
function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function MakeDuration({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry,
|
||||
);
|
||||
}
|
||||
|
||||
function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function MakeMultiLineField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
||||
@@ -101,12 +101,12 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEnt
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||
const initialValue = event[column.id as keyof OntimeEntry] ?? '';
|
||||
|
||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
return <MultiLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function LazyImage({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
||||
@@ -124,7 +124,7 @@ function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unkno
|
||||
return <EditableImage initialValue={initialValue} updateValue={update} />;
|
||||
}
|
||||
|
||||
function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function MakeSingleLineField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
||||
@@ -138,12 +138,12 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEn
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||
const initialValue = event[column.id as keyof OntimeEntry] ?? '';
|
||||
|
||||
return <SingleLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
return <SingleLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function MakeCustomField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
||||
@@ -161,7 +161,7 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
|
||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeEntry>[] {
|
||||
const dynamicCustomFields = Object.keys(customFields).map((key) => ({
|
||||
accessorKey: key,
|
||||
id: key,
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { Button, Checkbox } from '@chakra-ui/react';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import * as Editor from '../../../../features/editors/editor-utils/EditorUtils';
|
||||
|
||||
@@ -14,7 +14,7 @@ const buttonProps = {
|
||||
};
|
||||
|
||||
interface CuesheetTableSettingsProps {
|
||||
columns: Column<OntimeRundownEntry, unknown>[];
|
||||
columns: Column<OntimeEntry, unknown>[];
|
||||
handleResetResizing: () => void;
|
||||
handleResetReordering: () => void;
|
||||
handleClearToggles: () => void;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
|
||||
export default function useColumnManager(columns: ColumnDef<OntimeEntry>[]) {
|
||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||
key: 'table-order',
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
MaybeNumber,
|
||||
OntimeEntry,
|
||||
OntimeEntryCommonKeys,
|
||||
OntimeRundown,
|
||||
ProjectData,
|
||||
} from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
@@ -32,12 +32,8 @@ export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
||||
|
||||
/**
|
||||
* @description Creates an array of arrays usable by xlsx for export
|
||||
* @param {ProjectData} headerData
|
||||
* @param {OntimeRundown} rundown
|
||||
* @param {CustomFields} customFields
|
||||
* @return {(string[])[]}
|
||||
*/
|
||||
export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, customFields: CustomFields): string[][] => {
|
||||
export const makeTable = (headerData: ProjectData, rundown: OntimeEntry[], customFields: CustomFields): string[][] => {
|
||||
// create metadata header row
|
||||
const data = [['Ontime · Rundown export']];
|
||||
if (headerData.title) data.push([`Project title: ${headerData.title}`]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
import { useViewportSize } from '@mantine/hooks';
|
||||
import { isOntimeEvent, isPlayableEvent, OntimeRundown } from 'ontime-types';
|
||||
import { isOntimeEvent, isPlayableEvent, OntimeEntry } from 'ontime-types';
|
||||
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||
@@ -11,7 +11,7 @@ import style from './Timeline.module.scss';
|
||||
|
||||
interface TimelineProps {
|
||||
firstStart: number;
|
||||
rundown: OntimeRundown;
|
||||
rundown: OntimeEntry[];
|
||||
selectedEventId: string | null;
|
||||
totalDuration: number;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { isOntimeEvent, isPlayableEvent, MaybeString, OntimeEvent, OntimeRundown, PlayableEvent } from 'ontime-types';
|
||||
import { isOntimeEvent, isPlayableEvent, MaybeString, OntimeEntry, OntimeEvent, PlayableEvent } from 'ontime-types';
|
||||
import {
|
||||
dayInMs,
|
||||
getEventWithId,
|
||||
@@ -87,7 +87,7 @@ interface ScopedRundownData {
|
||||
totalDuration: number;
|
||||
}
|
||||
|
||||
export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeString): ScopedRundownData {
|
||||
export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeString): ScopedRundownData {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const data = useMemo(() => {
|
||||
@@ -102,7 +102,7 @@ export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeS
|
||||
let selectedIndex = selectedEventId ? Infinity : -1;
|
||||
let firstStart = null;
|
||||
let totalDuration = 0;
|
||||
let lastEntry: PlayableEvent | undefined;
|
||||
let lastEntry: PlayableEvent | null = null;
|
||||
|
||||
for (let i = 0; i < rundown.length; i++) {
|
||||
const currentEntry = rundown[i];
|
||||
@@ -164,7 +164,7 @@ type UpcomingEvents = {
|
||||
/**
|
||||
* Returns upcoming events from current: now, next and followedBy
|
||||
*/
|
||||
export function getUpcomingEvents(events: OntimeRundown, selectedId: MaybeString): UpcomingEvents {
|
||||
export function getUpcomingEvents(events: PlayableEvent[], selectedId: MaybeString): UpcomingEvents {
|
||||
if (events.length === 0) {
|
||||
return { now: null, next: null, followedBy: null };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user