From da7ef59216a174a52bbdeb253e96d310af7469f9 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:04:20 +0100 Subject: [PATCH] Custom fields views (#789) * style: show field colour in editor * feat: custom fields in cuesheet * feat: custom fields in operator * refactor: update CSV export --- .../view-params-editor/constants.ts | 27 +-- .../client/src/common/hooks/useEventAction.ts | 8 + apps/client/src/common/utils/fileDownload.ts | 11 +- .../src/features/cuesheet/CuesheetWrapper.tsx | 42 ++-- .../__snapshots__/utils.test.js.snap | 35 ++-- .../features/cuesheet/__tests__/utils.test.js | 27 +-- .../CuesheetHeader.tsx | 11 +- .../src/features/cuesheet/cuesheetCols.tsx | 89 +++------ .../src/features/cuesheet/cuesheetUtils.ts | 127 ++++++------ .../settings-modal/CuesheetSettingsForm.tsx | 183 ------------------ .../modals/settings-modal/SettingsModal.tsx | 5 - .../client/src/features/operator/Operator.tsx | 21 +- .../operator/edit-modal/EditModal.tsx | 13 +- .../operator/operator-event/OperatorEvent.tsx | 4 +- .../event-editor/EventEditor.module.scss | 14 ++ .../rundown/event-editor/EventEditor.tsx | 23 +-- .../composite/EventEditorUser.tsx | 83 -------- .../event-editor/composite/EventTextArea.tsx | 10 +- .../src/classes/data-provider/DataProvider.ts | 4 +- .../services/rundown-service/rundownCache.ts | 2 +- apps/server/src/utils/parser.ts | 2 +- e2e/tests/features/202-cuesheet.spec.ts | 27 +-- 22 files changed, 229 insertions(+), 539 deletions(-) delete mode 100644 apps/client/src/features/modals/settings-modal/CuesheetSettingsForm.tsx delete mode 100644 apps/client/src/features/rundown/event-editor/composite/EventEditorUser.tsx diff --git a/apps/client/src/common/components/view-params-editor/constants.ts b/apps/client/src/common/components/view-params-editor/constants.ts index 68bedc485..14325c156 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -1,4 +1,4 @@ -import { UserFields } from 'ontime-types'; +import { CustomFields } from 'ontime-types'; import { ParamField } from './types'; @@ -404,7 +404,11 @@ export const getStudioClockOptions = (timeFormat: string): ParamField[] => [ hideTimerSeconds, ]; -export const getOperatorOptions = (userFields: UserFields, timeFormat: string): ParamField[] => { +export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ParamField[] => { + const customFieldSelect = Object.keys(customFields).reduce((acc, key) => { + return { ...acc, [key]: key }; + }, {}); + return [ getTimeOption(timeFormat), { @@ -439,25 +443,14 @@ export const getOperatorOptions = (userFields: UserFields, timeFormat: string): { id: 'subscribe', title: 'Highlight Field', - description: 'Choose a field to highlight', + description: 'Choose a custom field to highlight', type: 'option', - values: { - user0: userFields.user0 || 'user0', - user1: userFields.user1 || 'user1', - user2: userFields.user2 || 'user2', - user3: userFields.user3 || 'user3', - user4: userFields.user4 || 'user4', - user5: userFields.user5 || 'user5', - user6: userFields.user6 || 'user6', - user7: userFields.user7 || 'user7', - user8: userFields.user8 || 'user8', - user9: userFields.user9 || 'user9', - }, + values: customFieldSelect, }, { id: 'shouldEdit', - title: 'Edit user field', - description: 'Allows editing an events user field by long pressing on it. Needs a selected highlighted field', + title: 'Edit custom field', + description: 'Allows editing an events selected custom field by long pressing.', type: 'boolean', defaultValue: false, }, diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index 1f300131e..996e4999c 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -150,6 +150,13 @@ export const useEventAction = () => { [_updateEventMutation], ); + const updateCustomField = useCallback( + async (eventId: string, field: string, value: string) => { + updateEvent({ id: eventId, custom: { [field]: { value } } }); + }, + [updateEvent], + ); + type TimeField = 'timeStart' | 'timeEnd' | 'duration'; /** * Updates time of existing event @@ -553,5 +560,6 @@ export const useEventAction = () => { swapEvents, updateEvent, updateTimer, + updateCustomField, }; }; diff --git a/apps/client/src/common/utils/fileDownload.ts b/apps/client/src/common/utils/fileDownload.ts index 38bd07b10..79cf83d81 100644 --- a/apps/client/src/common/utils/fileDownload.ts +++ b/apps/client/src/common/utils/fileDownload.ts @@ -11,6 +11,13 @@ type BlobOptions = { type: string; }; +/** + * Gets DB from backend and prepares a file to be downloaded + * @param url + * @param fileOptions + * @param blobOptions + * @returns + */ export default async function fileDownload(url: string, fileOptions: FileOptions, blobOptions: BlobOptions) { const response = await axios({ url: `${url}/db`, @@ -20,7 +27,7 @@ export default async function fileDownload(url: string, fileOptions: FileOptions const headerLine = response.headers['Content-Disposition']; let { name: fileName } = fileOptions; const { type: fileType } = fileOptions; - const { project, rundown, userFields } = response.data; + const { project, rundown, customFields } = response.data; // try and get the filename from the response if (headerLine != null) { @@ -37,7 +44,7 @@ export default async function fileDownload(url: string, fileOptions: FileOptions } if (fileType === 'csv') { - const sheetData = makeTable(project, rundown, userFields); + const sheetData = makeTable(project, rundown, customFields); fileContent = makeCSV(sheetData); fileName += '.csv'; } diff --git a/apps/client/src/features/cuesheet/CuesheetWrapper.tsx b/apps/client/src/features/cuesheet/CuesheetWrapper.tsx index e6b27ba0e..e7365425a 100644 --- a/apps/client/src/features/cuesheet/CuesheetWrapper.tsx +++ b/apps/client/src/features/cuesheet/CuesheetWrapper.tsx @@ -1,11 +1,11 @@ import { useCallback, useEffect, useMemo } from 'react'; -import { OntimeRundownEntry, ProjectData } from 'ontime-types'; +import { CustomFieldLabel, isOntimeEvent, ProjectData } from 'ontime-types'; import Empty from '../../common/components/state/Empty'; import { useEventAction } from '../../common/hooks/useEventAction'; import { useCuesheet } from '../../common/hooks/useSocket'; +import useCustomFields from '../../common/hooks-query/useCustomFields'; import { useFlatRundown } from '../../common/hooks-query/useRundown'; -import useUserFields from '../../common/hooks-query/useUserFields'; import CuesheetProgress from './cuesheet-progress/CuesheetProgress'; import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader'; @@ -18,18 +18,23 @@ import styles from './CuesheetWrapper.module.scss'; export default function CuesheetWrapper() { // TODO: can we use the normalised rundown for the table? const { data: flatRundown, status: rundownStatus } = useFlatRundown(); - const { data: userFields } = useUserFields(); - const { updateEvent } = useEventAction(); + const { data: customFields } = useCustomFields(); + + const { updateCustomField } = useEventAction(); const featureData = useCuesheet(); - const columns = useMemo(() => makeCuesheetColumns(userFields), [userFields]); + const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]); // Set window title useEffect(() => { document.title = 'ontime - Cuesheet'; }, []); + /** + * Handles updating a field + * Currently, only custom fields can be updated from the cuesheet + */ const handleUpdate = useCallback( - async (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: unknown) => { + async (rowIndex: number, accessor: CustomFieldLabel, payload: unknown) => { if (!flatRundown || rundownStatus !== 'success') { return; } @@ -40,42 +45,41 @@ export default function CuesheetWrapper() { // check if value is the same const event = flatRundown[rowIndex]; - if (!event) { + if (!event || !isOntimeEvent(event)) { return; } - if (event[accessor] === payload) { + const previousValue = event.custom[accessor]?.value; + + if (previousValue === payload) { return; } + // check if value is valid - // as of now, the fields do not have any validation + // in anticipation to different types of event here if (typeof payload !== 'string') { return; } // cleanup const cleanVal = payload.trim(); - const mutationObject = { - id: event.id, - [accessor]: cleanVal, - }; // submit try { - await updateEvent(mutationObject); + await updateCustomField(event.id, accessor, cleanVal); } catch (error) { console.error(error); } }, - [flatRundown, rundownStatus, updateEvent], + [flatRundown, rundownStatus, updateCustomField], ); const exportHandler = useCallback( (headerData: ProjectData) => { - if (!userFields || !flatRundown || rundownStatus !== 'success') { + if (!flatRundown || rundownStatus !== 'success') { return; } - const sheetData = makeTable(headerData, flatRundown, userFields); + const sheetData = makeTable(headerData, flatRundown, customFields); const csvContent = makeCSV(sheetData); const fileName = 'ontime rundown.csv'; @@ -92,10 +96,10 @@ export default function CuesheetWrapper() { URL.revokeObjectURL(url); return; }, - [flatRundown, rundownStatus, userFields], + [flatRundown, rundownStatus, customFields], ); - if (!userFields || !flatRundown || rundownStatus !== 'success') { + if (!customFields || !flatRundown || rundownStatus !== 'success') { return ; } diff --git a/apps/client/src/features/cuesheet/__tests__/__snapshots__/utils.test.js.snap b/apps/client/src/features/cuesheet/__tests__/__snapshots__/utils.test.js.snap index c648cdc1e..f6c089f1a 100644 --- a/apps/client/src/features/cuesheet/__tests__/__snapshots__/utils.test.js.snap +++ b/apps/client/src/features/cuesheet/__tests__/__snapshots__/utils.test.js.snap @@ -14,39 +14,32 @@ exports[`makeTable() > returns array of arrays with given fields 1`] = ` [ "Time Start", "Time End", - "Event Title", - "Presenter Name", - "Event Subtitle", - "Is Public? (x)", - "Note", + "Duration", + "ID", "Colour", - "End Action", - "Timer Type", + "Cue", + "Title", + "Subtitle", + "Presenter", + "Note", + "Is Public? (x)", "Skip?", - "user0:test", + "lighting", ], [ "00:00:00", "00:00:00", + "...", + "", + "", + "", "test title 1", "", "", + "", "x", "", "", - "", - "", - "", - "test", - "test", - "", - "", - "", - "", - "", - "", - "", - "", ], ] `; diff --git a/apps/client/src/features/cuesheet/__tests__/utils.test.js b/apps/client/src/features/cuesheet/__tests__/utils.test.js index d3a1e6f64..1c0937390 100644 --- a/apps/client/src/features/cuesheet/__tests__/utils.test.js +++ b/apps/client/src/features/cuesheet/__tests__/utils.test.js @@ -1,12 +1,13 @@ import { makeCSV, makeTable, parseField } from '../cuesheetUtils'; describe('parseField()', () => { - it('returns a string from given millis on timeStart and TimeEnd', () => { + it('returns a string from given millis on timeStart, TimeEnd and duration', () => { const testData1 = 1000; const testData2 = 60000; + const testData3 = 600000; expect(parseField('timeStart', testData1)).toBe('00:00:01'); expect(parseField('timeEnd', testData2)).toBe('00:01:00'); - expect(parseField('timeEnd', testData2)).toBe('00:01:00'); + expect(parseField('duration', testData3)).toBe('00:10:00'); }); describe('returns an x when isPublic is truthy, empty string otherwise', () => { @@ -31,22 +32,12 @@ describe('parseField()', () => { describe('simply returns any other value in any other field', () => { const testFields = [ - { field: 'nothing', value: 123 }, + { field: 'nothing', value: '123' }, { field: 'title', value: 'test' }, { field: 'presenter', value: 'test' }, { field: 'subtitle', value: 'test' }, { field: 'note', value: 'test' }, { field: 'colour', value: 'test' }, - { field: 'user0', value: 'test' }, - { field: 'user1', value: 'test' }, - { field: 'user2', value: 'test' }, - { field: 'user3', value: 'test' }, - { field: 'user4', value: 'test' }, - { field: 'user5', value: 'test' }, - { field: 'user6', value: 'test' }, - { field: 'user7', value: 'test' }, - { field: 'user8', value: 'test' }, - { field: 'user9', value: 'test' }, ]; testFields.forEach((testCase) => { @@ -70,15 +61,15 @@ describe('makeTable()', () => { timeStart: 0, timeEnd: 0, isPublic: 'x', - user0: 'test', - user1: 'test', + lighting: { value: 'test lighting' }, + sound: { value: 'test sound' }, }, ]; - const userFields = { - user0: 'test', + const customFields = { + lighting: { label: 'test' }, }; - const table = makeTable(headerData, tableData, userFields); + const table = makeTable(headerData, tableData, customFields); expect(table).toMatchSnapshot(); }); }); diff --git a/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx b/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx index 91d8e8248..f9d4455ab 100644 --- a/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx +++ b/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx @@ -15,6 +15,7 @@ import { flexRender, HeaderGroup } from '@tanstack/react-table'; import { OntimeRundownEntry } from 'ontime-types'; import { useLocalStorage } from '../../../common/hooks/useLocalStorage'; +import { getAccessibleColour } from '../../../common/utils/styleUtils'; import { tooltipDelayFast } from '../../../ontimeConfig'; import { initialColumnOrder } from '../cuesheetCols'; @@ -89,9 +90,17 @@ function CuesheetHeader(props: CuesheetHeaderProps) { {headerGroup.headers.map((header) => { const width = header.getSize(); + // @ts-expect-error -- we inject this into react-table + const customBackground = header.column.columnDef?.meta?.colour; + + let customStyles = {}; + if (customBackground) { + const customColour = getAccessibleColour(customBackground); + customStyles = { backgroundColor: customColour.backgroundColor, color: customColour.color }; + } return ( - + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ); diff --git a/apps/client/src/features/cuesheet/cuesheetCols.tsx b/apps/client/src/features/cuesheet/cuesheetCols.tsx index ed81742a4..709b42be7 100644 --- a/apps/client/src/features/cuesheet/cuesheetCols.tsx +++ b/apps/client/src/features/cuesheet/cuesheetCols.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark'; import { CellContext, ColumnDef } from '@tanstack/react-table'; -import { OntimeEvent, OntimeRundownEntry, UserFields } from 'ontime-types'; +import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types'; import { millisToString } from 'ontime-utils'; import DelayIndicator from '../../common/components/delay-indicator/DelayIndicator'; @@ -33,22 +33,36 @@ function MakeTimer({ getValue, row: { original } }: CellContext) { +function MakeCustomField({ row, column, table }: CellContext) { const update = useCallback( (newValue: string) => { // @ts-expect-error -- we inject this into react-table - table.options.meta?.handleUpdate(index, id, newValue); + table.options.meta?.handleUpdate(row.index, column.id, newValue); }, // eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable - [id, index], + [column.id, row.index], ); - const initialValue = getValue() as string; + const event = row.original; + if (!isOntimeEvent(event)) { + return null; + } + + // events dont necessarily contain all custom fields + const initialValue = event.custom[column.id]?.value ?? ''; return ; } -export function makeCuesheetColumns(userFields?: UserFields): ColumnDef[] { +export function makeCuesheetColumns(customFields: CustomFields): ColumnDef[] { + const dynamicCustomFields = Object.keys(customFields).map((key) => ({ + accessorKey: key, + id: key, + header: customFields[key].label, + meta: { colour: customFields[key].colour }, + cell: MakeCustomField, + })); + return [ { accessorKey: 'cue', @@ -109,67 +123,8 @@ export function makeCuesheetColumns(userFields?: UserFields): ColumnDef row.getValue(), }, - { - accessorKey: 'user0', - id: 'user0', - header: userFields?.user0 || 'User 0', - cell: MakeUserField, - }, - { - accessorKey: 'user1', - id: 'user1', - header: userFields?.user1 || 'User 1', - cell: MakeUserField, - }, - { - accessorKey: 'user2', - id: 'user2', - header: userFields?.user2 || 'User 2', - cell: MakeUserField, - }, - { - accessorKey: 'user3', - id: 'user3', - header: userFields?.user3 || 'User 3', - cell: MakeUserField, - }, - { - accessorKey: 'user4', - id: 'user4', - header: userFields?.user4 || 'User 4', - cell: MakeUserField, - }, - { - accessorKey: 'user5', - id: 'user5', - header: userFields?.user5 || 'User 5', - cell: MakeUserField, - }, - { - accessorKey: 'user6', - id: 'user6', - header: userFields?.user6 || 'User 6', - cell: MakeUserField, - }, - { - accessorKey: 'user7', - id: 'user7', - header: userFields?.user7 || 'User 7', - cell: MakeUserField, - }, - { - accessorKey: 'user8', - id: 'user8', - header: userFields?.user8 || 'User 8', - cell: MakeUserField, - }, - { - accessorKey: 'user9', - id: 'user9', - header: userFields?.user9 || 'User 9', - cell: MakeUserField, - }, + ...dynamicCustomFields, ]; } -export const initialColumnOrder: string[] = makeCuesheetColumns().map((column) => column.id as string); +export const initialColumnOrder: string[] = makeCuesheetColumns({}).map((column) => column.id as string); diff --git a/apps/client/src/features/cuesheet/cuesheetUtils.ts b/apps/client/src/features/cuesheet/cuesheetUtils.ts index 8fce1b31c..c06def0d8 100644 --- a/apps/client/src/features/cuesheet/cuesheetUtils.ts +++ b/apps/client/src/features/cuesheet/cuesheetUtils.ts @@ -1,7 +1,17 @@ import { stringify } from 'csv-stringify/browser/esm/sync'; -import { OntimeEntryCommonKeys, OntimeRundown, ProjectData, UserFields } from 'ontime-types'; +import { + CustomFields, + isOntimeDelay, + isOntimeEvent, + MaybeNumber, + OntimeEntryCommonKeys, + OntimeRundown, + ProjectData, +} from 'ontime-types'; import { millisToString } from 'ontime-utils'; +type CsvHeaderKey = OntimeEntryCommonKeys | keyof CustomFields; + /** * @description parses a field for export * @param {string} field @@ -9,90 +19,89 @@ import { millisToString } from 'ontime-utils'; * @return {string} */ -export const parseField = (field: T, data: unknown): string => { - let val; - switch (field) { - case 'timeStart': - case 'timeEnd': - val = millisToString(data as number | null); - break; - case 'isPublic': - case 'skip': - val = data ? 'x' : ''; - break; - default: - val = data; - break; +export const parseField = (field: CsvHeaderKey, data: unknown): string => { + if (field === 'timeStart' || field === 'timeEnd' || field === 'duration') { + return millisToString(data as MaybeNumber); } - if (typeof data === 'undefined') { - return ''; + + if (field === 'isPublic' || field === 'skip') { + return data ? 'x' : ''; } - // all other values are strings - return val as string; + + return String(data ?? ''); }; /** * @description Creates an array of arrays usable by xlsx for export - * @param {object} headerData - * @param {array} rundown - * @param {object} userFields + * @param {ProjectData} headerData + * @param {OntimeRundown} rundown + * @param {CustomFields} customFields * @return {(string[])[]} */ -export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, userFields: UserFields): string[][] => { +export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, customFields: CustomFields): string[][] => { + // create metadata header row const data = [['Ontime ยท Rundown export']]; if (headerData.title) data.push([`Project title: ${headerData.title}`]); if (headerData.description) data.push([`Project description: ${headerData.description}`]); - const fieldOrder: OntimeEntryCommonKeys[] = [ + const customFieldKeys = Object.keys(customFields).map((key) => `custom-${key}`); + const customFieldLabels = Object.keys(customFields); + + // we chose not to expose internals of the application + const fieldOrder: CsvHeaderKey[] = [ 'timeStart', 'timeEnd', - 'title', - 'presenter', - 'subtitle', - 'isPublic', - 'note', + 'duration', + 'id', 'colour', - 'endAction', - 'timerType', + 'cue', + 'title', + 'subtitle', + 'presenter', + 'note', + 'isPublic', 'skip', - 'user0', - 'user1', - 'user2', - 'user3', - 'user4', - 'user5', - 'user6', - 'user7', - 'user8', - 'user9', + ...customFieldKeys, ]; const fieldTitles = [ 'Time Start', 'Time End', - 'Event Title', - 'Presenter Name', - 'Event Subtitle', - 'Is Public? (x)', - 'Note', + 'Duration', + 'ID', 'Colour', - 'End Action', - 'Timer Type', + 'Cue', + 'Title', + 'Subtitle', + 'Presenter', + 'Note', + 'Is Public? (x)', 'Skip?', + ...customFieldLabels, ]; - for (const field in userFields) { - const fieldValue = userFields[field as keyof UserFields]; - const displayName = `${field}${fieldValue !== field && fieldValue !== '' ? `:${fieldValue}` : ''}`; - fieldTitles.push(displayName); - } - + // add header row to data data.push(fieldTitles); - rundown.forEach((entry) => { + if (isOntimeDelay(entry)) return; + const row: string[] = []; - // @ts-expect-error -- not sure how to type this - fieldOrder.forEach((field) => row.push(parseField(field, entry[field]))); + fieldOrder.forEach((field) => { + if (isOntimeEvent(entry)) { + // for custom fields, we need to extract the value from the custom object + if (field.startsWith('custom-')) { + const fieldLabel = field.split('custom-')[1]; + const value = entry.custom[fieldLabel]?.value; + row.push(parseField(fieldLabel, value)); + } else { + // @ts-expect-error -- it is ok, we will just not have the data for other fields + row.push(parseField(field, entry[field])); + } + return; + } + // @ts-expect-error -- it is ok, we will just not have the data for other fields + row.push(parseField(field, entry[field])); + }); data.push(row); }); @@ -101,10 +110,10 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, userF /** * @description Converts an array of arrays to a csv file - * @param {array[]} arrayOfArrays + * @param {string[][]} arrayOfArrays * @return {string} */ -export const makeCSV = (arrayOfArrays: string[][]) => { +export const makeCSV = (arrayOfArrays: string[][]): string => { const stringifiedData = stringify(arrayOfArrays); return stringifiedData; }; diff --git a/apps/client/src/features/modals/settings-modal/CuesheetSettingsForm.tsx b/apps/client/src/features/modals/settings-modal/CuesheetSettingsForm.tsx deleted file mode 100644 index 42c9f9634..000000000 --- a/apps/client/src/features/modals/settings-modal/CuesheetSettingsForm.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { Alert, AlertDescription, AlertIcon, AlertTitle, Input } from '@chakra-ui/react'; -import { UserFields } from 'ontime-types'; - -import { logAxiosError } from '../../../common/api/apiUtils'; -import { postUserFields } from '../../../common/api/ontimeApi'; -import useUserFields from '../../../common/hooks-query/useUserFields'; -import ModalLoader from '../modal-loader/ModalLoader'; -import { inputProps } from '../modalHelper'; -import ModalLink from '../ModalLink'; -import ModalSplitInput from '../ModalSplitInput'; -import OntimeModalFooter from '../OntimeModalFooter'; - -import style from './SettingsModal.module.scss'; - -const userFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields'; - -export default function CuesheetSettingsForm() { - const { data, status, isFetching, refetch } = useUserFields(); - const { - handleSubmit, - register, - reset, - formState: { errors, isSubmitting, isDirty, isValid }, - } = useForm({ - defaultValues: data, - values: data, - resetOptions: { - keepDirtyValues: true, - }, - }); - - useEffect(() => { - if (data) { - reset(data); - } - }, [data, reset]); - - const onSubmit = async (formData: UserFields) => { - try { - await postUserFields(formData); - } catch (error) { - logAxiosError('Error saving cuesheet settings', error); - } finally { - await refetch(); - } - }; - - const onReset = () => { - reset(data); - }; - - const disableInputs = status === 'pending'; - - if (isFetching) { - return ; - } - - return ( -
-
- - -
- User Fields - - Allow for custom naming of additional data fields on each event (eg. light, sound, camera).
- See the docs -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/client/src/features/modals/settings-modal/SettingsModal.tsx b/apps/client/src/features/modals/settings-modal/SettingsModal.tsx index 97e560aa1..3b11c9170 100644 --- a/apps/client/src/features/modals/settings-modal/SettingsModal.tsx +++ b/apps/client/src/features/modals/settings-modal/SettingsModal.tsx @@ -4,7 +4,6 @@ import ModalWrapper from '../ModalWrapper'; import AliasesForm from './AliasesForm'; import AppSettingsModal from './AppSettings'; -import CuesheetSettingsForm from './CuesheetSettingsForm'; import EditorSettings from './EditorSettings'; import ProjectDataForm from './ProjectDataForm'; import ViewSettingsForm from './ViewSettingsForm'; @@ -24,7 +23,6 @@ export default function SettingsModal(props: ModalManagerProps) { App Project Data Editor - Cuesheet Views URL Aliases @@ -38,9 +36,6 @@ export default function SettingsModal(props: ModalManagerProps) { - - - diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index c86d73c3e..c6ea04770 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { isOntimeEvent, OntimeEvent, SupportedEvent, UserFields } from 'ontime-types'; +import { CustomFields, isOntimeEvent, OntimeEvent, SupportedEvent, UserFields } from 'ontime-types'; import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils'; import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu'; @@ -9,10 +9,10 @@ import { getOperatorOptions } from '../../common/components/view-params-editor/c import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import useFollowComponent from '../../common/hooks/useFollowComponent'; import { useOperator } from '../../common/hooks/useSocket'; +import useCustomFields from '../../common/hooks-query/useCustomFields'; import useProjectData from '../../common/hooks-query/useProjectData'; import useRundown from '../../common/hooks-query/useRundown'; import useSettings from '../../common/hooks-query/useSettings'; -import useUserFields from '../../common/hooks-query/useUserFields'; import { debounce } from '../../common/utils/debounce'; import { getDefaultFormat } from '../../common/utils/time'; import { isStringBoolean } from '../../common/utils/viewUtils'; @@ -30,12 +30,12 @@ const selectedOffset = 50; type TitleFields = Pick; export type EditEvent = Pick & { fieldLabel?: string; fieldValue: string }; export type PartialEdit = EditEvent & { - field: keyof UserFields; + field: keyof CustomFields; }; export default function Operator() { const { data, status } = useRundown(); - const { data: userFields, status: userFieldsStatus } = useUserFields(); + const { data: customFields, status: customFieldStatus } = useCustomFields(); const { data: projectData, status: projectDataStatus } = useProjectData(); const timeoutId = useRef(null); @@ -116,8 +116,8 @@ export default function Operator() { [searchParams], ); - const missingData = !data || !userFields || !projectData; - const isLoading = status === 'pending' || userFieldsStatus === 'pending' || projectDataStatus === 'pending'; + const missingData = !data || !customFields || !projectData; + const isLoading = status === 'pending' || customFieldStatus === 'pending' || projectDataStatus === 'pending'; if (missingData || isLoading) { return ; @@ -125,15 +125,14 @@ export default function Operator() { // get fields which the user subscribed to const shouldEdit = searchParams.get('shouldEdit'); - const subscribe = searchParams.get('subscribe') as keyof UserFields | null; + const subscribe = searchParams.get('subscribe') as keyof CustomFields; const canEdit = shouldEdit && subscribe; const main = searchParams.get('main') as keyof TitleFields | null; const secondary = searchParams.get('secondary') as keyof TitleFields | null; - const subscribedAlias = subscribe ? userFields[subscribe] : ''; const defaultFormat = getDefaultFormat(settings?.timeFormat); - const operatorOptions = getOperatorOptions(userFields, defaultFormat); + const operatorOptions = getOperatorOptions(customFields, defaultFormat); let isPast = Boolean(featureData.selectedEventId); const hidePast = isStringBoolean(searchParams.get('hidepast')); @@ -178,7 +177,7 @@ export default function Operator() { const mainField = main ? entry?.[main] || entry.title : entry.title; const secondaryField = secondary ? entry?.[secondary] || entry.subtitle : entry.subtitle; - const subscribedData = (subscribe ? entry?.[subscribe] : undefined) || ''; + const subscribedData = entry.custom[subscribe]?.value; return ( undefined} diff --git a/apps/client/src/features/operator/edit-modal/EditModal.tsx b/apps/client/src/features/operator/edit-modal/EditModal.tsx index e82d99f59..5da59a202 100644 --- a/apps/client/src/features/operator/edit-modal/EditModal.tsx +++ b/apps/client/src/features/operator/edit-modal/EditModal.tsx @@ -1,6 +1,5 @@ import { useRef, useState } from 'react'; import { Button, Textarea } from '@chakra-ui/react'; -import { OntimeEvent } from 'ontime-types'; import { useEventAction } from '../../../common/hooks/useEventAction'; import type { PartialEdit } from '../Operator'; @@ -15,19 +14,18 @@ interface EditModalProps { export default function EditModal(props: EditModalProps) { const { event, onClose } = props; - const { updateEvent } = useEventAction(); + const { updateCustomField } = useEventAction(); const [loading, setLoading] = useState(false); const inputRef = useRef(null); const handleSave = async () => { setLoading(true); const newValue = inputRef.current?.value; + if (newValue === undefined) { + return; + } - const partialEvent: Partial = { - id: event.id, - [event.field]: newValue, - }; - await updateEvent(partialEvent); + await updateCustomField(event.id, event.field, newValue); setLoading(false); onClose(); }; @@ -43,6 +41,7 @@ export default function EditModal(props: EditModalProps) { placeholder={`Add value for ${fieldLabel} field`} defaultValue={event.fieldValue} isDisabled={loading} + resize='none' />
-
{`/ontime/load/id "${event.id}"`} diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorUser.tsx b/apps/client/src/features/rundown/event-editor/composite/EventEditorUser.tsx deleted file mode 100644 index db3725ad0..000000000 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorUser.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { UserFields } from 'ontime-types'; - -import useUserFields from '../../../../common/hooks-query/useUserFields'; -import { EditorUpdateFields } from '../EventEditor'; - -import EventTextArea from './EventTextArea'; - -import style from '../EventEditor.module.scss'; - -interface EventEditorUserProps { - userFields: UserFields; - handleSubmit: (field: EditorUpdateFields, value: string) => void; -} - -export default function EventEditorUser(props: EventEditorUserProps) { - const { userFields, handleSubmit } = props; - const { data } = useUserFields(); - - return ( -
- - - - - - - - - - -
- ); -} diff --git a/apps/client/src/features/rundown/event-editor/composite/EventTextArea.tsx b/apps/client/src/features/rundown/event-editor/composite/EventTextArea.tsx index d356c06f5..64ad94888 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventTextArea.tsx +++ b/apps/client/src/features/rundown/event-editor/composite/EventTextArea.tsx @@ -1,28 +1,32 @@ -import { useCallback } from 'react'; +import { CSSProperties, useCallback } from 'react'; import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea'; import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput'; +import { cx } from '../../../../common/utils/styleUtils'; import { EditorUpdateFields } from '../EventEditor'; import style from '../EventEditor.module.scss'; interface CountedTextAreaProps { + className?: string; field: EditorUpdateFields; label: string; initialValue: string; + style?: CSSProperties; submitHandler: (field: EditorUpdateFields, value: string) => void; } export default function EventTextArea(props: CountedTextAreaProps) { - const { field, label, initialValue, submitHandler } = props; + const { className, field, label, initialValue, style: givenStyles, submitHandler } = props; const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]); const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback); + const classes = cx([style.inputLabel, className]); return (
-