mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 00:13:53 +00:00
Custom fields views (#789)
* style: show field colour in editor * feat: custom fields in cuesheet * feat: custom fields in operator * refactor: update CSV export
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 <Empty text='Loading...' />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||
{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 (
|
||||
<SortableCell key={header.column.columnDef.id} header={header} style={{ width }}>
|
||||
<SortableCell key={header.column.columnDef.id} header={header} style={{ width, ...customStyles }}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</SortableCell>
|
||||
);
|
||||
|
||||
@@ -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<OntimeRundownEnt
|
||||
);
|
||||
}
|
||||
|
||||
function MakeUserField({ getValue, row: { index }, column: { id }, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
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 <EditableCell value={initialValue} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
export function makeCuesheetColumns(userFields?: UserFields): ColumnDef<OntimeRundownEntry>[] {
|
||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
||||
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<OntimeRu
|
||||
header: 'Note',
|
||||
cell: (row) => 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);
|
||||
|
||||
@@ -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 = <T extends OntimeEntryCommonKeys>(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;
|
||||
};
|
||||
|
||||
@@ -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<UserFields>({
|
||||
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 <ModalLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} id='cuesheet-settings' className={style.sectionContainer}>
|
||||
<div style={{ height: '16px' }} />
|
||||
<Alert status='info' variant='ontime-on-light-info'>
|
||||
<AlertIcon />
|
||||
<div className={style.column}>
|
||||
<AlertTitle>User Fields</AlertTitle>
|
||||
<AlertDescription>
|
||||
Allow for custom naming of additional data fields on each event (eg. light, sound, camera). <br />
|
||||
<ModalLink href={userFieldsDocsUrl}>See the docs</ModalLink>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</Alert>
|
||||
<div style={{ height: '16px' }} />
|
||||
<ModalSplitInput field='user0' title='User0' description='' error={errors.user0?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user0')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user1' title='User1' description='' error={errors.user1?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user1')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user2' title='User2' description='' error={errors.user2?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user2')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user3' title='User3' description='' error={errors.user3?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user3')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user4' title='User4' description='' error={errors.user4?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user4')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user5' title='User5' description='' error={errors.user5?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user5')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user6' title='User6' description='' error={errors.user6?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user6')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user7' title='User7' description='' error={errors.user7?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user7')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user8' title='User8' description='' error={errors.user8?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user8')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='user9' title='User9' description='' error={errors.user9?.message}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
width='300px'
|
||||
variant='ontime-filled-on-light'
|
||||
isDisabled={disableInputs}
|
||||
placeholder='Display name for user field'
|
||||
{...register('user9')}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<OntimeModalFooter
|
||||
formId='cuesheet-settings'
|
||||
handleRevert={onReset}
|
||||
isDirty={isDirty}
|
||||
isValid={isValid}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
<Tab>App</Tab>
|
||||
<Tab>Project Data</Tab>
|
||||
<Tab>Editor</Tab>
|
||||
<Tab>Cuesheet</Tab>
|
||||
<Tab>Views</Tab>
|
||||
<Tab>URL Aliases</Tab>
|
||||
</TabList>
|
||||
@@ -38,9 +36,6 @@ export default function SettingsModal(props: ModalManagerProps) {
|
||||
<TabPanel>
|
||||
<EditorSettings />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CuesheetSettingsForm />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ViewSettingsForm />
|
||||
</TabPanel>
|
||||
|
||||
@@ -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<OntimeEvent, 'title' | 'subtitle' | 'presenter'>;
|
||||
export type EditEvent = Pick<OntimeEvent, 'id' | 'cue'> & { 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<NodeJS.Timeout | null>(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 <Empty text='Loading...' />;
|
||||
@@ -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 (
|
||||
<OperatorEvent
|
||||
@@ -194,7 +193,7 @@ export default function Operator() {
|
||||
delay={entry.delay}
|
||||
isSelected={isSelected}
|
||||
subscribed={subscribedData}
|
||||
subscribedAlias={subscribedAlias}
|
||||
subscribeLabel={subscribe}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
onLongPress={canEdit ? handleEdit : () => undefined}
|
||||
|
||||
@@ -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<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
const newValue = inputRef.current?.value;
|
||||
if (newValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const partialEvent: Partial<OntimeEvent> = {
|
||||
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'
|
||||
/>
|
||||
<div className={style.buttonRow}>
|
||||
<Button variant='ontime-subtle' onClick={onClose} isDisabled={loading}>
|
||||
|
||||
@@ -22,7 +22,7 @@ interface OperatorEventProps {
|
||||
delay?: number;
|
||||
isSelected: boolean;
|
||||
subscribed?: string;
|
||||
subscribedAlias: string;
|
||||
subscribeLabel: string;
|
||||
isPast: boolean;
|
||||
selectedRef?: RefObject<HTMLDivElement>;
|
||||
onLongPress: (event: EditEvent) => void;
|
||||
@@ -47,7 +47,7 @@ function OperatorEvent(props: OperatorEventProps) {
|
||||
delay,
|
||||
isSelected,
|
||||
subscribed,
|
||||
subscribedAlias,
|
||||
subscribeLabel: subscribedAlias,
|
||||
isPast,
|
||||
selectedRef,
|
||||
onLongPress,
|
||||
|
||||
@@ -77,3 +77,17 @@
|
||||
column-gap: 1.5rem;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.decorated {
|
||||
&::before{
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
margin-right: 0.25rem;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 99px;
|
||||
background-color: var(--decorator-color, $gray-1100);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CSSProperties, useCallback, useEffect, useState } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
import EventEditorTitles from './composite/EventEditorTitles';
|
||||
import EventEditorUser from './composite/EventEditorUser';
|
||||
import EventTextArea from './composite/EventTextArea';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
@@ -86,21 +85,6 @@ export default function EventEditor() {
|
||||
);
|
||||
}
|
||||
|
||||
// Compositing user fields by hand
|
||||
// this will be replaced by dynamic logic
|
||||
const userFields = {
|
||||
user0: event.user0,
|
||||
user1: event.user1,
|
||||
user2: event.user2,
|
||||
user3: event.user3,
|
||||
user4: event.user4,
|
||||
user5: event.user5,
|
||||
user6: event.user6,
|
||||
user7: event.user7,
|
||||
user8: event.user8,
|
||||
user9: event.user9,
|
||||
};
|
||||
|
||||
const customKeys = Object.keys(customFields ?? {});
|
||||
|
||||
return (
|
||||
@@ -142,16 +126,17 @@ export default function EventEditor() {
|
||||
{customKeys.map((label) => {
|
||||
return (
|
||||
<EventTextArea
|
||||
key={label}
|
||||
key={`${event.id}-${label}`}
|
||||
field={`custom-${label}`}
|
||||
label={label}
|
||||
initialValue={event.custom[label]?.value ?? ''}
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
style={{ '--decorator-color': customFields[label].colour } as CSSProperties}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<EventEditorUser key={`${event.id}-user`} userFields={userFields} handleSubmit={handleSubmit} />
|
||||
</div>
|
||||
<div className={style.footer}>
|
||||
<CopyTag label='OSC trigger by id'>{`/ontime/load/id "${event.id}"`}</CopyTag>
|
||||
|
||||
@@ -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 (
|
||||
<div className={style.column}>
|
||||
<EventTextArea
|
||||
field='user0'
|
||||
label={data?.user0 ?? 'user0'}
|
||||
initialValue={userFields.user0}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user1'
|
||||
label={data?.user1 ?? 'user1'}
|
||||
initialValue={userFields.user1}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user2'
|
||||
label={data?.user2 ?? 'user2'}
|
||||
initialValue={userFields.user2}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user3'
|
||||
label={data?.user3 ?? 'user3'}
|
||||
initialValue={userFields.user3}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user4'
|
||||
label={data?.user4 ?? 'user4'}
|
||||
initialValue={userFields.user4}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user5'
|
||||
label={data?.user5 ?? 'user5'}
|
||||
initialValue={userFields.user5}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user6'
|
||||
label={data?.user6 ?? 'user6'}
|
||||
initialValue={userFields.user6}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user7'
|
||||
label={data?.user7 ?? 'user7'}
|
||||
initialValue={userFields.user7}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user8'
|
||||
label={data?.user8 ?? 'user8'}
|
||||
initialValue={userFields.user8}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
<EventTextArea
|
||||
field='user9'
|
||||
label={data?.user9 ?? 'user9'}
|
||||
initialValue={userFields.user9}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<label className={style.inputLabel} htmlFor={field}>
|
||||
<label className={classes} htmlFor={field} style={givenStyles}>
|
||||
{label}
|
||||
</label>
|
||||
<AutoTextArea
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
|
||||
import { data, db } from '../../modules/loadDb.js';
|
||||
import { safeMerge } from './DataProvider.utils.js';
|
||||
import { isProduction } from '../../setup.js';
|
||||
import { isTest } from '../../setup.js';
|
||||
|
||||
export class DataProvider {
|
||||
static getData() {
|
||||
@@ -110,7 +110,7 @@ export class DataProvider {
|
||||
}
|
||||
|
||||
static async persist() {
|
||||
if (!isProduction) {
|
||||
if (isTest) {
|
||||
return;
|
||||
}
|
||||
await db.write();
|
||||
|
||||
@@ -220,6 +220,7 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||
// TODO: should we trottle this?
|
||||
// defer writing to the database
|
||||
setImmediate(() => {
|
||||
console.log('writing to database', persistedRundown.length)
|
||||
DataProvider.setRundown(persistedRundown);
|
||||
});
|
||||
|
||||
@@ -283,7 +284,6 @@ export function edit({ persistedRundown, eventId, patch }: EditArgs): Required<M
|
||||
|
||||
const eventInMemory = persistedRundown[indexAt];
|
||||
const newEvent = makeEvent(eventInMemory, patch);
|
||||
console.log('got', patch, 'will make', newEvent);
|
||||
|
||||
const newRundown = [...persistedRundown];
|
||||
newRundown[indexAt] = newEvent;
|
||||
|
||||
@@ -402,7 +402,7 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
||||
revision: originalEvent.revision,
|
||||
timeWarning: patchEvent.timeWarning ?? originalEvent.timeWarning,
|
||||
timeDanger: patchEvent.timeDanger ?? originalEvent.timeDanger,
|
||||
custom: patchEvent.custom ?? originalEvent.custom,
|
||||
custom: { ...originalEvent.custom, ...patchEvent.custom },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,25 +20,16 @@ test('cuesheet displays events and exports csv', async ({ page }) => {
|
||||
const expectedColumns = [
|
||||
'Time Start',
|
||||
'Time End',
|
||||
'Event Title',
|
||||
'Presenter Name',
|
||||
'Event Subtitle',
|
||||
'Public',
|
||||
'Note',
|
||||
'Duration',
|
||||
'ID',
|
||||
'Colour',
|
||||
'End Action',
|
||||
'Timer Type',
|
||||
'Skip',
|
||||
'user0',
|
||||
'user1',
|
||||
'user2',
|
||||
'user3',
|
||||
'user4',
|
||||
'user5',
|
||||
'user6',
|
||||
'user7',
|
||||
'user8',
|
||||
'user9',
|
||||
'Cue',
|
||||
'Title',
|
||||
'Subtitle',
|
||||
'Presenter',
|
||||
'Note',
|
||||
'Is Public? (x)',
|
||||
'Skip?',
|
||||
];
|
||||
const expectedValues = ['Albania', 'Latvia', 'Lithuania', 'Lunch break'];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user