From b1838a5e9a57ef22d5fc11777732efc8db145af9 Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Wed, 12 Jun 2024 14:25:26 +0200 Subject: [PATCH] display multiple custom fields in operator view (#1001) --------- Co-authored-by: Carlos Valente --- .../view-params-editor/ParamInput.tsx | 63 ++++++++++++++++++- .../view-params-editor/constants.ts | 11 ++-- .../components/view-params-editor/types.ts | 9 ++- .../client/src/features/operator/Operator.tsx | 40 ++++++------ .../operator/edit-modal/EditModal.tsx | 59 +++++++++++------ .../operator-event/OperatorEvent.module.scss | 21 +++++-- .../operator/operator-event/OperatorEvent.tsx | 32 ++++++---- .../src/features/viewers/common/viewUtils.ts | 4 -- pnpm-lock.yaml | 4 +- 9 files changed, 171 insertions(+), 72 deletions(-) diff --git a/apps/client/src/common/components/view-params-editor/ParamInput.tsx b/apps/client/src/common/components/view-params-editor/ParamInput.tsx index 24013d963..56380e85b 100644 --- a/apps/client/src/common/components/view-params-editor/ParamInput.tsx +++ b/apps/client/src/common/components/view-params-editor/ParamInput.tsx @@ -1,5 +1,18 @@ +import { useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Input, InputGroup, InputLeftElement, Select, Switch } from '@chakra-ui/react'; +import { + Button, + Input, + InputGroup, + InputLeftElement, + Menu, + MenuButton, + MenuItemOption, + MenuList, + MenuOptionGroup, + Select, + Switch, +} from '@chakra-ui/react'; import { isStringBoolean } from '../../../features/viewers/common/viewUtils'; @@ -34,6 +47,10 @@ export default function ParamInput(props: EditFormInputProps) { ); } + if (type === 'multi-option') { + return ; + } + if (type === 'boolean') { const defaultCheckedValue = isStringBoolean(searchParams.get(id)) || defaultValue; @@ -70,3 +87,47 @@ export default function ParamInput(props: EditFormInputProps) { ); } + +interface EditFormMultiOptionProps { + paramField: ParamField & { type: 'multi-option' }; +} + +function MultiOption(props: EditFormMultiOptionProps) { + const [searchParams] = useSearchParams(); + const { paramField } = props; + const { id, defaultValue } = paramField; + + const optionFromParams = (searchParams.get(id) ?? '').toLocaleLowerCase(); + const defaultOptionValue = optionFromParams || defaultValue?.toLocaleLowerCase() || ''; + + const [paramState, setParamState] = useState(defaultOptionValue); + + return ( + <> + + + + {paramField.title} + + + { + setParamState(typeof value === 'object' ? value.filter((v) => v !== '').join('_') : value); + }} + > + {Object.values(paramField.values).map((option) => { + const { value, label } = option; + return ( + + {label} + + ); + })} + + + + + ); +} 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 e69bab721..5f35063ba 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -1,8 +1,6 @@ import { CustomFields } from 'ontime-types'; -import { capitaliseFirstLetter } from '../../../features/viewers/common/viewUtils'; - -import { ParamField } from './types'; +import { type ParamField } from './types'; const makeOptionsFromCustomFields = (customFields: CustomFields, additionalOptions?: Record) => { const customFieldOptions = Object.entries(customFields).reduce((acc, [key, value]) => { @@ -464,8 +462,8 @@ export const getStudioClockOptions = (timeFormat: string): ParamField[] => [ export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ParamField[] => { const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' }); - const customFieldSelect = Object.keys(customFields).reduce((acc, key) => { - return { ...acc, [key]: `Custom: ${capitaliseFirstLetter(key)}` }; + const customFieldSelect = Object.entries(customFields).reduce((acc, [key, field]) => { + return { ...acc, [key]: { value: key, label: field.label, colour: field.colour } }; }, {}); return [ @@ -497,9 +495,8 @@ export const getOperatorOptions = (customFields: CustomFields, timeFormat: strin id: 'subscribe', title: 'Highlight Field', description: 'Choose a custom field to highlight', - type: 'option', + type: 'multi-option', values: customFieldSelect, - defaultValue: '', }, { id: 'shouldEdit', diff --git a/apps/client/src/common/components/view-params-editor/types.ts b/apps/client/src/common/components/view-params-editor/types.ts index 9405297da..ab80f3e1f 100644 --- a/apps/client/src/common/components/view-params-editor/types.ts +++ b/apps/client/src/common/components/view-params-editor/types.ts @@ -9,8 +9,15 @@ type OptionsField = { values: Record; defaultValue?: string; }; + +type MultiOptionsField = { + type: 'multi-option'; + values: Record; + defaultValue?: string; +}; + type StringField = { type: 'string'; defaultValue?: string; prefix?: string; placeholder?: string }; type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string }; type BooleanField = { type: 'boolean'; defaultValue: boolean }; -export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField); +export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField | MultiOptionsField); diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index 74c6d9404..b95cc488a 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 { CustomField, CustomFields, isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types'; +import { isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types'; import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils'; import Empty from '../../common/components/state/Empty'; @@ -27,11 +27,9 @@ import style from './Operator.module.scss'; const selectedOffset = 50; +export type Subscribed = { id: string; label: string; colour: string; value: string }[]; type TitleFields = Pick; -export type EditEvent = Pick & { fieldLabel?: string; fieldValue: string }; -export type PartialEdit = EditEvent & { - field: keyof CustomFields; -}; +export type EditEvent = Pick & { subscriptions: Subscribed }; export default function Operator() { const { data, status } = useRundown(); @@ -45,7 +43,7 @@ export default function Operator() { const { data: settings } = useSettings(); const [showEditPrompt, setShowEditPrompt] = useState(false); - const [editEvent, setEditEvent] = useState(null); + const [editEvent, setEditEvent] = useState(null); const [lockAutoScroll, setLockAutoScroll] = useState(false); const selectedRef = useRef(null); @@ -102,16 +100,9 @@ export default function Operator() { debouncedHandleScroll(); }; - const handleEdit = useCallback( - (event: EditEvent) => { - const field = searchParams.get('subscribe') as keyof CustomField | null; - - if (field) { - setEditEvent({ ...event, field }); - } - }, - [searchParams], - ); + const handleEdit = useCallback((event: EditEvent) => { + setEditEvent({ ...event }); + }, []); const missingData = !data || !customFields || !projectData; const isLoading = status === 'pending' || customFieldStatus === 'pending' || projectDataStatus === 'pending'; @@ -122,8 +113,13 @@ export default function Operator() { // get fields which the user subscribed to const shouldEdit = searchParams.get('shouldEdit'); - const subscribe = searchParams.get('subscribe') as keyof CustomFields; - const canEdit = shouldEdit && subscribe; + + const subscriptions = (searchParams.get('subscribe') ?? '') + .toLocaleLowerCase() + .split('_') + .filter((value) => Object.hasOwn(customFields, value)); + + const canEdit = shouldEdit && subscriptions; const main = searchParams.get('main') as keyof TitleFields | null; const secondary = searchParams.get('secondary'); @@ -173,7 +169,12 @@ export default function Operator() { const mainField = main ? getPropertyValue(entry, main) ?? '' : entry.title; const secondaryField = getPropertyValue(entry, secondary) ?? ''; - const subscribedData = entry.custom[subscribe]; + const subscribedData = subscriptions + ? subscriptions.map((id) => { + const { label, colour } = customFields[id]; + return { id, label, colour, value: entry.custom[id] }; + }) + : null; 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 5da59a202..a059ee81e 100644 --- a/apps/client/src/features/operator/edit-modal/EditModal.tsx +++ b/apps/client/src/features/operator/edit-modal/EditModal.tsx @@ -1,48 +1,69 @@ 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'; +import type { EditEvent } from '../Operator'; import style from './EditModal.module.scss'; interface EditModalProps { - event: PartialEdit; + event: EditEvent; onClose: () => void; } export default function EditModal(props: EditModalProps) { const { event, onClose } = props; - const { updateCustomField } = useEventAction(); + const { updateEvent } = useEventAction(); const [loading, setLoading] = useState(false); - const inputRef = useRef(null); + const inputRef = useRef(new Array()); const handleSave = async () => { + if (!inputRef.current) return; setLoading(true); - const newValue = inputRef.current?.value; - if (newValue === undefined) { - return; + + const patchObject: Partial = { id: event.id }; + + inputRef.current.forEach((element) => { + if (element.dataset.field && element.defaultValue != element.value) { + if (patchObject.custom) { + patchObject.custom[element.dataset.field] = element.value; + } else { + Object.assign(patchObject, { custom: { [element.dataset.field]: element.value } }); + } + } + }); + + if (patchObject.custom) { + await updateEvent(patchObject); } - await updateCustomField(event.id, event.field, newValue); setLoading(false); onClose(); }; - const fieldLabel = event?.fieldLabel ?? event.field; - return (
-
{`Editing field ${fieldLabel} in cue ${event.cue}`}
-