diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index 0d30165950..54dac5d6d9 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -8,10 +8,35 @@ import { DefaultUserOperationsTypes } from './ingest.js' * Description of a user performed editing operation allowed on an document */ export type UserEditingDefinition = + | UserEditingDefinitionState | UserEditingDefinitionAction | UserEditingDefinitionForm | UserEditingDefinitionSofieDefault +/** + * A simple 'state' that can be signlaled to the user, but has no associated action. This is useful for indicating + * things like "This piece is being held" or "This piece is being affected by a global action" + */ +export interface UserEditingDefinitionState { + type: UserEditingType.STATE + /** Id of this operation */ + id: string + /** Label to show to the user for this operation */ + label: ITranslatableMessage + /** Icon to show when this action is 'active' + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL + */ + icon?: string + /** Icon to show when this action is 'disabled' + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL + */ + iconInactive?: string + /** Whether this action should be indicated as being active */ + isActive?: boolean +} + /** * A simple 'action' that can be performed */ @@ -65,6 +90,8 @@ export interface UserEditingDefinitionSofieDefault { } export enum UserEditingType { + /** State */ + STATE = 'state', /** Action */ ACTION = 'action', /** Form */ diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index fe0911e986..89336137ff 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -8,10 +8,29 @@ import type { import type { ITranslatableMessage } from '../TranslatableMessage.js' export type CoreUserEditingDefinition = + | CoreUserEditingDefinitionState | CoreUserEditingDefinitionAction | CoreUserEditingDefinitionForm | CoreUserEditingDefinitionSofie +export interface CoreUserEditingDefinitionState { + type: UserEditingType.STATE + /** Id of this operation */ + id: string + /** Label to show to the user for this operation */ + label: ITranslatableMessage + /** Icon to show when this state is 'active'. + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL */ + icon?: string + /** Icon to show when this state is 'disabled'. + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL */ + iconInactive?: string + /** Whether this state should be indicated as being active */ + isActive?: boolean +} + export interface CoreUserEditingDefinitionAction { type: UserEditingType.ACTION /** Id of this operation */ diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index ce8bf026c8..6ad2780a38 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -15,6 +15,7 @@ import { import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { CoreUserEditingDefinition, + CoreUserEditingDefinitionState, CoreUserEditingDefinitionAction, CoreUserEditingDefinitionForm, CoreUserEditingProperties, @@ -71,6 +72,7 @@ import { UserEditingProperties, UserEditingDefinitionSofieDefault, UserEditingType, + UserEditingDefinitionState, } from '@sofie-automation/blueprints-integration/dist/userEditing' import type { PlayoutMutatablePart } from '../../playout/model/PlayoutPartInstanceModel.js' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' @@ -568,6 +570,15 @@ function translateUserEditsToBlueprint( return _.compact( userEdits.map((userEdit) => { switch (userEdit.type) { + case UserEditingType.STATE: + return literal({ + type: UserEditingType.STATE, + id: userEdit.id, + label: omit(userEdit.label, 'namespaces'), + icon: userEdit.icon, + iconInactive: userEdit.iconInactive, + isActive: userEdit.isActive, + }) case UserEditingType.ACTION: return literal({ type: UserEditingType.ACTION, @@ -631,6 +642,15 @@ export function translateUserEditsFromBlueprint( return _.compact( userEdits.map((userEdit) => { switch (userEdit.type) { + case UserEditingType.STATE: + return literal({ + type: UserEditingType.STATE, + id: userEdit.id, + label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds), + icon: userEdit.icon, + iconInactive: userEdit.iconInactive, + isActive: userEdit.isActive, + }) case UserEditingType.ACTION: return literal({ type: UserEditingType.ACTION, diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index e2210e2222..6502ea86de 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -289,20 +289,20 @@ export class TSRHandler { return `Device "${device?.deviceName ?? id}" (${device?.instanceId ?? 'instance unknown'}): ` + e } - const fixError = (id: string, e: Error): any => { + const fixError = (id: string, e: any): any => { const device = this._coreTsrHandlers[id]?._device const name = `Device "${device?.deviceName ?? id}" (${device?.instanceId ?? 'instance unknown'})` - if (!e || !('message' in e)) { + if (!e || typeof e !== 'object' || !('message' in e)) { return { - message: name + ': ' + 'Unknown error: ' + JSON.stringify(e), + message: name + ': ' + 'Unknown error: ' + stringifyError(e, true), } } return { - message: e.message && name + ': ' + e.message, - name: e.name && name + ': ' + e.name, - stack: e.stack && e.stack + '\nAt device' + name, + message: e.message !== undefined ? name + ': ' + e.message : undefined, + name: e.name !== undefined ? name + ': ' + e.name : undefined, + stack: e.stack !== undefined ? e.stack + '\nAt device' + name : undefined, } } const fixContext = (...context: any[]): any => { diff --git a/packages/shared-lib/src/lib/JSONSchemaUtil.ts b/packages/shared-lib/src/lib/JSONSchemaUtil.ts index b952845baa..7142670a4f 100644 --- a/packages/shared-lib/src/lib/JSONSchemaUtil.ts +++ b/packages/shared-lib/src/lib/JSONSchemaUtil.ts @@ -12,6 +12,10 @@ export enum SchemaFormUIField { * Title of the property */ Title = 'ui:title', + /** + * Icon to use for the property, for widgets that support them: `oneOfButtons` in `oneOf` array members + */ + Icon = 'ui:icon', /** * Description/hint for the property */ @@ -24,15 +28,27 @@ export enum SchemaFormUIField { * If an integer property, whether to treat it as zero-based */ ZeroBased = 'ui:zeroBased', + /** + * Whether the property is read-only. This will disable the input and hide any buttons for modifying the value. + */ + ReadOnly = 'ui:readOnly', /** * Override the presentation with a special mode. * Currently only valid for: - * - object properties. Valid values are 'json'. - * - string properties. Valid values are 'base64-image'. - * - boolean properties. Valid values are 'switch'. - * - array properties with items.type string. Valid values are 'bread-crumbs'. + * - object properties. Valid values are `json`, `oneOfButtons`. + * - `oneOfButtons` uses a `oneOf` list of possible variants of the object, with a `ui:oneOf:discriminant` field + * to determine which variant is selected. + * - string properties. Valid values are `base64-image`. + * - boolean properties. Valid values are `switch`. + * - number properties. Valid values are `timeMs`. + * - array properties with items.type string. Valid values are `bread-crumbs`. */ DisplayType = 'ui:displayType', + /** + * When using `oneOf` for an object, the discriminant field is used to determine which variant is selected. + * The value of this field must be a property that has a unique const value for each variant in the oneOf + */ + OneOfDiscriminant = 'ui:oneOf:discriminant', /** * Name of the enum values as generated for the typescript enum. * Future: a new field should probably be added for the UI to use. diff --git a/packages/webui/src/client/lib/Components/Button.tsx b/packages/webui/src/client/lib/Components/Button.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webui/src/client/lib/Components/FloatInput.tsx b/packages/webui/src/client/lib/Components/FloatInput.tsx index 4a7ab77e4a..d2be6ef248 100644 --- a/packages/webui/src/client/lib/Components/FloatInput.tsx +++ b/packages/webui/src/client/lib/Components/FloatInput.tsx @@ -6,6 +6,7 @@ interface IFloatInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string /** Call handleUpdate on every change, before focus is lost */ @@ -24,6 +25,7 @@ export function FloatInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, handleUpdate, updateOnKey, @@ -36,6 +38,8 @@ export function FloatInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + const number = parseFloat(event.target.value.replace(',', '.')) setEditingValue(number) @@ -43,10 +47,15 @@ export function FloatInputControl({ handleUpdate(zeroBased ? number - 1 : number) } }, - [handleUpdate, updateOnKey, zeroBased] + [handleUpdate, updateOnKey, zeroBased, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) { + setEditingValue(null) + return + } + const number = parseFloat(event.currentTarget.value.replace(',', '.')) if (!isNaN(number)) { handleUpdate(zeroBased ? number - 1 : number) @@ -54,13 +63,19 @@ export function FloatInputControl({ setEditingValue(null) }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] + ) + const handleFocus = useCallback( + (event: React.FocusEvent) => { + if (readOnly) return + setEditingValue(parseFloat(event.currentTarget.value.replace(',', '.'))) + }, + [readOnly] ) - const handleFocus = useCallback((event: React.FocusEvent) => { - setEditingValue(parseFloat(event.currentTarget.value.replace(',', '.'))) - }, []) const handleKeyUp = useCallback( (event: React.KeyboardEvent) => { + if (readOnly) return + if (event.key === 'Escape') { setEditingValue(null) } else if (event.key === 'Enter') { @@ -70,7 +85,7 @@ export function FloatInputControl({ } } }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] ) let showValue: string | number | undefined = editingValue ?? undefined @@ -93,6 +108,7 @@ export function FloatInputControl({ onFocus={handleFocus} onKeyUp={handleKeyUp} disabled={disabled} + readOnly={readOnly} /> ) } diff --git a/packages/webui/src/client/lib/Components/IntInput.tsx b/packages/webui/src/client/lib/Components/IntInput.tsx index d607e7032c..b37ec42f5a 100644 --- a/packages/webui/src/client/lib/Components/IntInput.tsx +++ b/packages/webui/src/client/lib/Components/IntInput.tsx @@ -6,6 +6,7 @@ interface IIntInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string /** Call handleUpdate on every change, before focus is lost */ @@ -24,6 +25,7 @@ export function IntInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, handleUpdate, updateOnKey, @@ -36,6 +38,8 @@ export function IntInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + const number = parseInt(event.target.value, 10) setEditingValue(number) @@ -43,10 +47,12 @@ export function IntInputControl({ handleUpdate(zeroBased ? number - 1 : number) } }, - [handleUpdate, updateOnKey, zeroBased] + [handleUpdate, updateOnKey, zeroBased, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) return + const number = parseInt(event.currentTarget.value, 10) if (!isNaN(number)) { handleUpdate(zeroBased ? number - 1 : number) @@ -54,13 +60,15 @@ export function IntInputControl({ setEditingValue(null) }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] ) const handleFocus = useCallback((event: React.FocusEvent) => { setEditingValue(parseInt(event.currentTarget.value, 10)) }, []) const handleKeyUp = useCallback( (event: React.KeyboardEvent) => { + if (readOnly) return + if (event.key === 'Escape') { setEditingValue(null) } else if (event.key === 'Enter') { @@ -70,7 +78,7 @@ export function IntInputControl({ } } }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] ) let showValue: string | number | undefined = editingValue ?? undefined @@ -93,6 +101,7 @@ export function IntInputControl({ onFocus={handleFocus} onKeyUp={handleKeyUp} disabled={disabled} + readOnly={readOnly} /> ) } diff --git a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx index e9695e95a8..39302c8fbb 100644 --- a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx +++ b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx @@ -20,6 +20,7 @@ interface IMultiLineTextInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string /** Call handleUpdate on every change, before focus is lost */ @@ -33,6 +34,7 @@ export function MultiLineTextInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, handleUpdate, updateOnKey, @@ -41,20 +43,24 @@ export function MultiLineTextInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + setEditingValue(event.target.value) if (updateOnKey) { handleUpdate(splitValueIntoLines(event.target.value)) } }, - [handleUpdate, updateOnKey] + [handleUpdate, updateOnKey, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) return + handleUpdate(splitValueIntoLines(event.target.value)) setEditingValue(null) }, - [handleUpdate] + [handleUpdate, readOnly] ) const handleFocus = useCallback((event: React.FocusEvent) => { setEditingValue(event.currentTarget.value) @@ -82,6 +88,7 @@ export function MultiLineTextInputControl({ onKeyUp={handleKeyUp} onKeyPress={handleKeyPress} disabled={disabled} + readOnly={readOnly} /> ) } diff --git a/packages/webui/src/client/lib/Components/TextInput.tsx b/packages/webui/src/client/lib/Components/TextInput.tsx index 175c443003..bffcb19a9a 100644 --- a/packages/webui/src/client/lib/Components/TextInput.tsx +++ b/packages/webui/src/client/lib/Components/TextInput.tsx @@ -13,6 +13,7 @@ interface ITextInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string spellCheck?: boolean @@ -29,6 +30,7 @@ export function TextInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, spellCheck, suggestions, @@ -39,16 +41,20 @@ export function TextInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + setEditingValue(event.target.value) if (updateOnKey) { handleUpdate(event.target.value) } }, - [handleUpdate, updateOnKey] + [handleUpdate, updateOnKey, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) return + let value: string = event.target.value if (value) { value = value.trim() @@ -57,20 +63,22 @@ export function TextInputControl({ setEditingValue(null) }, - [handleUpdate] + [handleUpdate, readOnly] ) const handleFocus = useCallback((event: React.FocusEvent) => { setEditingValue(event.currentTarget.value) }, []) const handleKeyUp = useCallback( (event: React.KeyboardEvent) => { + if (readOnly) return + if (event.key === 'Escape') { setEditingValue(null) } else if (event.key === 'Enter') { handleUpdate(event.currentTarget.value) } }, - [handleUpdate] + [handleUpdate, readOnly] ) const fieldId = useMemo(() => getRandomString(), []) @@ -85,6 +93,7 @@ export function TextInputControl({ onBlur={handleBlur} onFocus={handleFocus} onKeyUp={handleKeyUp} + readOnly={readOnly} disabled={disabled} spellCheck={spellCheck} list={suggestions ? fieldId : undefined} diff --git a/packages/webui/src/client/lib/Components/TimeMsInput.tsx b/packages/webui/src/client/lib/Components/TimeMsInput.tsx new file mode 100644 index 0000000000..8ccb387624 --- /dev/null +++ b/packages/webui/src/client/lib/Components/TimeMsInput.tsx @@ -0,0 +1,187 @@ +import { useCallback, useState } from 'react' +import ClassNames from 'classnames' +import Form from 'react-bootstrap/Form' + +interface ITimeMsInputControlProps { + classNames?: string + modifiedClassName?: string + disabled?: boolean + readOnly?: boolean + placeholder?: string + + /** Call handleUpdate on every change, before focus is lost */ + updateOnKey?: boolean + + value: number | undefined + handleUpdate: (value: number) => void + + min?: number + max?: number + multipleOf?: number +} + +const ALLOWED_KEYS = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '.', + ':', + ',', + 'Backspace', + 'Tab', + 'Enter', + 'Escape', + 'ArrowLeft', + 'ArrowRight', + 'Home', + 'End', +] + +function formatTime(time: number, opts?: { showZeroHours?: boolean; showZeroFrames?: boolean }): string { + const frames = time % 1000 + const ms = String(frames).padStart(3, '0') + const ss = String(Math.floor(time / 1000) % 60).padStart(2, '0') + const mm = String(Math.floor(time / 60000) % 60).padStart(2, '0') + const hours = Math.floor(time / 3600000) + + let result = `${mm}:${ss}` + if (frames > 0 || opts?.showZeroFrames) { + result += `.${ms}` + } + if (hours > 0 || opts?.showZeroHours) { + const hh = String(hours).padStart(2, '0') + result = `${hh}:${result}` + } + + return result +} + +function parseTime(time: string): number { + const parts = time.split(':').map((part) => part.trim()) + const partsCount = parts.length + if (partsCount > 3) return Number.NaN + + let ms = 0 + for (let i = 0; i < partsCount; i++) { + const part = parts[partsCount - 1 - i] + const number = parseInt(part, 10) + if (i === 0 && part.includes('.')) { + const number = parseFloat(part) + if (isNaN(number) || number < 0) return Number.NaN + ms += number * 1000 + } else if (isNaN(number) || number < 0) return Number.NaN + else if (i === 0 && partsCount) ms += number * 1000 + else if (i === 1) ms += number * 60000 + else if (i === 2) ms += number * 3600000 + } + + return ms +} + +export function TimeMsInputControl({ + classNames, + modifiedClassName, + value, + disabled, + readOnly, + placeholder, + handleUpdate, + updateOnKey, + min, + max, + multipleOf, +}: Readonly): JSX.Element { + const [editingValue, setEditingValue] = useState(null) + + const isValidValue = useCallback( + (value: number): boolean => { + if (isNaN(value) || value < 0) return false + if (min !== undefined && value < min) return false + if (max !== undefined && value > max) return false + if (multipleOf !== undefined && value % multipleOf !== 0) return false + return true + }, + [min, max, multipleOf] + ) + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + if (readOnly) return + + const number = parseTime(event.target.value) + setEditingValue(event.target.value) + + if (updateOnKey && !isNaN(number) && isValidValue(number)) { + handleUpdate(number) + } + }, + [handleUpdate, updateOnKey, isValidValue, readOnly] + ) + const handleBlur = useCallback( + (event: React.FocusEvent) => { + if (readOnly) return + const number = parseTime(event.currentTarget.value) + if (!isNaN(number) && isValidValue(number)) { + handleUpdate(number) + } + + setEditingValue(null) + }, + [handleUpdate, isValidValue, readOnly] + ) + const handleFocus = useCallback((event: React.FocusEvent) => { + setEditingValue(event.currentTarget.value) + event.currentTarget.selectionStart = 0 + event.currentTarget.selectionEnd = event.currentTarget.value.length + }, []) + const handleKeyUp = useCallback( + (event: React.KeyboardEvent) => { + if (readOnly) return + + if (event.key === 'Escape') { + setEditingValue(null) + } else if (event.key === 'Enter') { + const number = parseTime(event.currentTarget.value) + if (!isNaN(number) && isValidValue(number)) { + handleUpdate(number) + } + } + }, + [handleUpdate, isValidValue, readOnly] + ) + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + // allow ctrl/cmd + any key, to allow for shortcuts like ctrl+a, ctrl+c, ctrl+v, etc. + if (!ALLOWED_KEYS.includes(event.key) && event.ctrlKey === false && event.metaKey === false) { + event.preventDefault() + } + }, []) + + let showValue: string | number | undefined = editingValue ?? undefined + if (showValue === undefined && value !== undefined) { + showValue = formatTime(value) + } + if (showValue === undefined) showValue = '' + + return ( + + ) +} diff --git a/packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.scss b/packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.scss new file mode 100644 index 0000000000..42dc0e5dda --- /dev/null +++ b/packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.scss @@ -0,0 +1,44 @@ +.field-one-of-buttons .field-content { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-items: flex-start; + gap: 0.5em; +} + +.field-one-of-button-complex { + font-size: 1em; +} + +.field-one-of-button-complex, .field-one-of-button-complex .field { + display: flex; + flex-direction: row; + align-items: start; + gap: 0.5em; +} + +.field-one-of-button-complex .field .field-content input[type="text"].form-control { + max-width: 10ch; +} + +.field-one-of-button-complex > .btn-outline-primary { + display: flex; + align-items: center; + white-space: nowrap; + background-color: var(--org-color-gray-600); + color: var(--org-color-gray-400); + box-sizing: border-box; + border-bottom: 3px solid transparent; + padding-bottom: calc(var(--bs-btn-padding-y) - 3px); + gap: 0.2em; + + > .svg > svg { + height: 1.2em; + width: auto; + } +} + +.field-one-of-button-complex > .btn-check-checked { + border-bottom: 3px solid var(--org-color-primary); +} diff --git a/packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.tsx b/packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.tsx new file mode 100644 index 0000000000..bdd5a2ed02 --- /dev/null +++ b/packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.tsx @@ -0,0 +1,332 @@ +import { faQuestionCircle, faSync } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { getSchemaUIField, type JSONSchema, SchemaFormUIField } from '@sofie-automation/blueprints-integration' +import { objectPathGet } from '@sofie-automation/corelib/dist/lib' +import classNames from 'classnames' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import Button from 'react-bootstrap/Button' +import { useTranslation } from 'react-i18next' +import type { + OverrideOpHelperForItemContents, + WrappedOverridableItemNormal, +} from '../../../ui/Settings/util/OverrideOpHelper.js' +import { LabelActual, type LabelAndOverridesProps } from '../../Components/LabelAndOverrides.js' +import { hasOpWithPath } from '../../Components/util.js' +import { type SchemaFormCommonProps, translateStringIfHasNamespaces } from '../schemaFormUtil.js' +import { SchemaFormWithState } from '../SchemaFormWithState.js' +import { TypeName } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' +import { BlueprintAssetIcon } from '../../Components/BlueprintAssetIcon.js' + +export const OneOfButtonsWithOverrides = ( + props: Readonly & { + /** Base path of the schema within the document */ + attr: string + + /** The wrapped item to be edited, with its overrides */ + item: WrappedOverridableItemNormal + /** Helper to generate and save overrides for the item */ + overrideHelper: OverrideOpHelperForItemContents + } +): JSX.Element => { + const { t } = useTranslation() + + const discProperty = getSchemaUIField(props.schema, SchemaFormUIField.OneOfDiscriminant) + + const childProps = useMemo(() => { + const title = getSchemaUIField(props.schema, SchemaFormUIField.Title) || props.schema.title || props.attr + const description = getSchemaUIField(props.schema, SchemaFormUIField.Description) + + return { + schema: props.schema, + translationNamespaces: props.translationNamespaces, + commonAttrs: { + label: translateStringIfHasNamespaces(title, props.translationNamespaces), + hint: description ? translateStringIfHasNamespaces(description, props.translationNamespaces) : undefined, + item: props.item, + itemKey: props.attr, + overrideHelper: props.overrideHelper, + + showClearButton: !!props.showClearButtonForNonRequiredFields && !props.isRequired, + }, + isRequired: props.isRequired, + } + }, [ + props.schema, + props.translationNamespaces, + props.attr, + props.item, + props.overrideHelper, + props.isRequired, + props.showClearButtonForNonRequiredFields, + ]) + + if (!discProperty) { + return ( +

+ {t('Property "{{ propLabel }}" does not have a discriminant property specified for variants', { + propLabel: childProps.commonAttrs.label, + })} +

+ ) + } + + if (!props.schema.oneOf) { + return ( +

+ {t('Property "{{ propLabel }}" does not have oneOf variants declared', { + propLabel: childProps.commonAttrs.label, + })} +

+ ) + } + + const invalidVariantIndex = props.schema.oneOf.findIndex( + (variant) => variant.type !== TypeName.Object || variant.properties?.[discProperty].const === undefined + ) + if (invalidVariantIndex >= 0) { + return ( +

+ {t( + 'Property "{{ propLabel }}" has a variant at index {{ index }} that is not an object and/or does not specify a discriminant', + { + propLabel: childProps.commonAttrs.label, + index: invalidVariantIndex, + } + )} +

+ ) + } + + return ( + + {(value, handleUpdate) => { + return ( + props.schema.oneOf && + props.schema.oneOf.map((variant, index) => { + const type = variant.properties?.[discProperty]?.const + if (type === undefined) + return ( +

+ {t( + 'Discriminant property "{{ discProperty }}" used, but is undefined for variant at index {{ index }}', + { + discProperty, + index, + } + )} +

+ ) + + return ( + + ) + }) + ) + }} +
+ ) +} + +function OneOfVariantButtonComplex({ + schema, + selected, + discProperty, + translationNamespaces, + value, + handleUpdate, +}: Readonly<{ + discProperty: string + schema: JSONSchema + translationNamespaces: string[] + selected: boolean + value: any + handleUpdate?: (update: Record) => void +}>): JSX.Element { + const typeValue = schema.properties?.[discProperty]?.const + + const handleUpdateRef = useRef(handleUpdate) + + useEffect(() => { + handleUpdateRef.current = handleUpdate + }, [handleUpdate]) + + const [editingValue, setEditingValue] = useState>( + selected + ? (value ?? { + [discProperty]: typeValue, + }) + : { + [discProperty]: typeValue, + } + ) + + const oldValue = useRef(value) + + useEffect(() => { + setEditingValue((val) => ({ + ...val, + [discProperty]: typeValue, + })) + }, [discProperty, typeValue]) + + useEffect(() => { + if (selected && value !== undefined && oldValue.current !== value && oldValue.current === undefined) { + handleUpdateRef.current?.(editingValue) + } else if (selected && value !== undefined && oldValue.current !== value) { + setEditingValue(value) + } + + oldValue.current = value + }, [value, discProperty, typeValue, editingValue, selected]) + + const variantTitle = getSchemaUIField(schema, SchemaFormUIField.Title) + const variantIcon = getSchemaUIField(schema, SchemaFormUIField.Icon) + + const onUpdateLocal = useCallback( + (update: Record) => { + if (!selected) { + setEditingValue(() => ({ + ...update, + })) + } else { + handleUpdateRef.current?.(update) + } + }, + [selected] + ) + + const handleSelect = useCallback(() => { + if (!selected) { + handleUpdateRef.current?.(editingValue) + } + }, [editingValue, discProperty, typeValue, selected]) + + return ( + + ) +} + +function LabelAndOverridesForOneOfButtons({ + children, + label, + hint, + item, + itemKey, + overrideHelper, + showClearButton, + formatDefaultValue, +}: Readonly>): JSX.Element { + const { t } = useTranslation() + + const clearOverride = useCallback(() => { + overrideHelper().clearItemOverrides(item.id, String(itemKey)).commit() + }, [overrideHelper, item.id, itemKey]) + const setValue = useCallback( + (newValue: any) => { + overrideHelper().setItemValue(item.id, String(itemKey), newValue).commit() + }, + [overrideHelper, item.id, itemKey] + ) + + const isOverridden = hasOpWithPath(item.overrideOps, item.id, String(itemKey)) + + let displayValue: JSX.Element | string | null = '""' + if (item.defaults) { + const defaultValue: any = objectPathGet(item.defaults, String(itemKey)) + // Special cases for formatting of the default + if (formatDefaultValue) { + displayValue = formatDefaultValue(defaultValue) + } else if (defaultValue === false) { + displayValue = 'false' + } else if (defaultValue === true) { + displayValue = 'true' + } else if (!defaultValue) { + displayValue = '' + } else if (Array.isArray(defaultValue) || typeof defaultValue === 'object') { + displayValue = JSON.stringify(defaultValue) || '' + } else { + // Display it as a string + displayValue = `${defaultValue}` + } + } + + const value = objectPathGet(item.computed, String(itemKey)) + + return ( +
+ + +
+ {showClearButton && ( + + )} + + {children(value, setValue)} +
+ + {item.defaults && ( + <> + + {displayValue === null ? ( + + ) : typeof displayValue === 'object' ? ( + displayValue + ) : ( + + )} + + + + )} + + {hint && {hint}} +
+ ) +} diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx index 8f98b3294c..1d2e300fe5 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx @@ -34,6 +34,8 @@ import { Base64ImageInputControl } from '../Components/Base64ImageInput.js' import { MultiLineIntInputControl } from '../Components/MultiLineIntInput.js' import { ToggleSwitchControl } from '../Components/ToggleSwitch.js' import { BreadCrumbTextInput } from '../Components/BreadCrumbTextInput.js' +import { OneOfButtonsWithOverrides } from './SchemaFormOneOfButtons/OneOfButtons.js' +import { TimeMsInputControl } from '../Components/TimeMsInput.js' interface SchemaFormWithOverridesProps extends SchemaFormCommonProps { /** Base path of the schema within the document */ @@ -58,6 +60,7 @@ interface FormComponentProps { /** Whether a clear button should be showed for fields not marked as "required" */ showClearButton: boolean + readOnly: boolean } /** Whether this field has been marked as "required" */ @@ -80,6 +83,7 @@ function useChildPropsForFormComponent(props: Readonly): JSX.Element { +export function SchemaFormWithOverrides(props: Readonly): JSX.Element | null { const { t } = useTranslation() const childProps = useChildPropsForFormComponent(props) + if (props.schema.const) { + return null + } + switch (props.schema.type) { case TypeName.Array: if ( @@ -112,6 +120,11 @@ export function SchemaFormWithOverrides(props: Readonly + } else if ( + getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'oneOfButtons' && + props.schema.oneOf + ) { + return } else if (props.schema.patternProperties) { if (props.allowTables) { return @@ -128,7 +141,11 @@ export function SchemaFormWithOverrides(props: Readonly } case TypeName.Number: - return + if (getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'timeMs') { + return + } else { + return + } case TypeName.Boolean: if (getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'switch') { return @@ -330,12 +347,31 @@ const IntegerFormWithOverrides = ({ schema, commonAttrs }: Readonly )} ) } +const TimeMsFormWithOverrides = ({ schema, commonAttrs }: Readonly) => { + return ( + + {(value, handleUpdate) => ( + + )} + + ) +} + const NumberFormWithOverrides = ({ schema, commonAttrs }: Readonly) => { return ( @@ -346,6 +382,7 @@ const NumberFormWithOverrides = ({ schema, commonAttrs }: Readonly )} @@ -377,7 +414,12 @@ const StringFormWithOverrides = ({ schema, commonAttrs }: Readonly {(value, handleUpdate) => ( - + )} ) @@ -391,6 +433,7 @@ const StringArrayFormWithOverrides = ({ schema, commonAttrs }: Readonly )} diff --git a/packages/webui/src/client/styles/main.scss b/packages/webui/src/client/styles/main.scss index 6b38bc9597..d9a9c9be2c 100644 --- a/packages/webui/src/client/styles/main.scss +++ b/packages/webui/src/client/styles/main.scss @@ -83,37 +83,38 @@ input { @import 'shelf/textLabelPanel'; @import 'shelf/timeOfDayPanel'; -@import '../lib/forms/SchemaFormTable/ObjectTable.scss'; -@import '../lib/Components/PromiseButton.scss'; -@import '../lib/VideoPreviewPlayer.scss'; -@import '../ui/ClipTrimPanel/ClipTrimPanel.scss'; -@import '../ui/ClipTrimPanel/TimecodeEncoder.scss'; -@import '../ui/ClipTrimPanel/VideoEditMonitor.scss'; -@import '../ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.scss'; -@import '../ui/SegmentStoryboard/SegmentStoryboard.scss'; -@import '../ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardPartSecondaryPieces.scss'; -@import '../ui/Settings/components/triggeredActions/TriggeredActionsEditor.scss'; -@import '../ui/Settings/Forms.scss'; -@import '../ui/SegmentTimeline/SegmentTimeline.scss'; -@import '../ui/Status/media-status/MediaStatusListItem.scss'; -@import '../ui/Status/media-status/MediaStatusList.scss'; -@import '../ui/Status/media-status/MediaStatusListHeader.scss'; -@import '../ui/Status/package-status/package-status.scss'; -@import '../ui/SegmentList/LinePartPieceIndicator/LinePartPieceIndicator.scss'; -@import '../ui/SegmentList/LinePartPieceIndicator/PieceIndicatorMenu.scss'; -@import '../ui/SegmentList/SegmentList.scss'; -@import '../ui/SegmentList/LinePartMainPiece/LinePartMainPiece.scss'; -@import '../ui/SegmentList/LinePartTransitionPiece/LinePartTransitionPiece.scss'; -@import '../ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.scss'; -@import '../ui/PieceIcons/IconColors.scss'; +@import '../lib/forms/SchemaFormTable/ObjectTable'; +@import '../lib/forms/SchemaFormOneOfButtons/OneOfButtons'; +@import '../lib/Components/PromiseButton'; +@import '../lib/VideoPreviewPlayer'; +@import '../ui/ClipTrimPanel/ClipTrimPanel'; +@import '../ui/ClipTrimPanel/TimecodeEncoder'; +@import '../ui/ClipTrimPanel/VideoEditMonitor'; +@import '../ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail'; +@import '../ui/SegmentStoryboard/SegmentStoryboard'; +@import '../ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardPartSecondaryPieces'; +@import '../ui/Settings/components/triggeredActions/TriggeredActionsEditor'; +@import '../ui/Settings/Forms'; +@import '../ui/SegmentTimeline/SegmentTimeline'; +@import '../ui/Status/media-status/MediaStatusListItem'; +@import '../ui/Status/media-status/MediaStatusList'; +@import '../ui/Status/media-status/MediaStatusListHeader'; +@import '../ui/Status/package-status/package-status'; +@import '../ui/SegmentList/LinePartPieceIndicator/LinePartPieceIndicator'; +@import '../ui/SegmentList/LinePartPieceIndicator/PieceIndicatorMenu'; +@import '../ui/SegmentList/SegmentList'; +@import '../ui/SegmentList/LinePartMainPiece/LinePartMainPiece'; +@import '../ui/SegmentList/LinePartTransitionPiece/LinePartTransitionPiece'; +@import '../ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece'; +@import '../ui/PieceIcons/IconColors'; @import '../ui/PieceIcons/PieceIcons'; @import '../ui/ClockView/ClockViewPieceIcons/ClockViewPieceIcons'; -@import '../ui/ClockView/CameraScreen/CameraScreen.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.scss'; -@import '../ui/SegmentAdlibTesting/SegmentAdlibTesting.scss'; +@import '../ui/ClockView/CameraScreen/CameraScreen'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUp'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule'; +@import '../ui/SegmentAdlibTesting/SegmentAdlibTesting'; @import 'rundownView'; diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index d56a62f6ed..bdde984086 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -68,7 +68,12 @@ letter-spacing: 0.5px; > .svg { + width: 1em; flex-shrink: 0; + + > svg { + display: block; + } } > .title { flex-grow: 1; diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index d6807e77b1..01a764e108 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -2115,6 +2115,21 @@ svg.icon { > .label-icon.label-loop-icon { margin: 0 0 0 0; } + + > .label-icon.label-custom-icon { + margin-top: -3px; + display: flex; + flex-direction: row-reverse; + + > div { + flex: 0 0; + + > svg { + height: 1em; + width: auto; + } + } + } } &.last-words { diff --git a/packages/webui/src/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx b/packages/webui/src/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx index 23202575c9..3f236351c6 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Renderers/CustomLayerItemRenderer.tsx @@ -5,10 +5,18 @@ import type { ISourceLayerUi, IOutputLayerUi, PartUi } from '../SegmentTimelineC import { RundownUtils } from '../../../lib/rundown.js' import { faCut } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { PieceLifespan, type VTContent } from '@sofie-automation/blueprints-integration' +import { PieceLifespan, UserEditingType, type VTContent } from '@sofie-automation/blueprints-integration' import type { OffsetPosition } from '../../../utils/positions.js' import { LoopingPieceIcon } from '../../../lib/ui/icons/looping.js' import type { PieceUi } from '@sofie-automation/corelib/src/dataModel/Piece.js' +import { BlueprintAssetIcon } from '../../../lib/Components/BlueprintAssetIcon.js' +import type { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep.js' +import type { + CoreUserEditingDefinitionAction, + CoreUserEditingDefinitionForm, + CoreUserEditingDefinitionSofie, + CoreUserEditingDefinitionState, +} from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' export type SourceDurationLabelAlignment = 'left' | 'right' @@ -118,6 +126,65 @@ export class CustomLayerItemRenderer } + private operationWithUsefulIcon( + op: + | ReadonlyObjectDeep + | ReadonlyObjectDeep + | ReadonlyObjectDeep + | ReadonlyObjectDeep + ): op is ReadonlyObjectDeep | ReadonlyObjectDeep { + return ( + ((op.type === UserEditingType.ACTION || op.type === UserEditingType.STATE) && + ((op.icon && op.isActive) || (op.iconInactive && !op.isActive))) || + false + ) + } + + protected customPieceIconsChanged(prevProps: Readonly): boolean { + if (this.props.piece.instance.piece.userEditOperations === prevProps.piece.instance.piece.userEditOperations) { + return false + } + + if ( + this.props.piece.instance.piece.userEditOperations?.length !== + prevProps.piece.instance.piece.userEditOperations?.length + ) { + return true + } + + const currentIconSignature = + this.props.piece.instance.piece.userEditOperations + ?.filter(this.operationWithUsefulIcon) + ?.map((op) => `${op.id}:${op.isActive}:${op.icon ?? ''}:${op.iconInactive ?? ''}`) + .join('|') ?? '' + const prevIconSignature = + prevProps.piece.instance.piece.userEditOperations + ?.filter(this.operationWithUsefulIcon) + ?.map((op) => `${op.id}:${op.isActive}:${op.icon ?? ''}:${op.iconInactive ?? ''}`) + .join('|') ?? '' + + return currentIconSignature !== prevIconSignature + } + + protected renderCustomPieceIcons(): JSX.Element | null { + if ( + !this.props.piece.instance.piece.userEditOperations || + this.props.piece.instance.piece.userEditOperations.length === 0 + ) + return null + + return ( + <> + {this.props.piece.instance.piece.userEditOperations.filter(this.operationWithUsefulIcon).map((op) => ( +
+ {op.isActive && op.icon && } + {!op.isActive && op.iconInactive && } +
+ ))} + + ) + } + protected renderOverflowTimeLabel(): JSX.Element | null { const overflowTime = this.doesOverflowTime() if ( diff --git a/packages/webui/src/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx b/packages/webui/src/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx index a0650ec25d..e3258f86d2 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Renderers/DefaultLayerItemRenderer.tsx @@ -33,7 +33,10 @@ export class DefaultLayerItemRenderer extends CustomLayerItemRenderer + {this.renderCustomPieceIcons()} {this.renderInfiniteIcon()} {this.renderOverflowTimeLabel()} diff --git a/packages/webui/src/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx b/packages/webui/src/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx index 359bdfb4d2..cd994e0d34 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Renderers/L3rdSourceRenderer.tsx @@ -150,6 +150,7 @@ export class L3rdSourceRenderer extends CustomLayerItemRenderer ref={this.setRightLabelRef} style={this.getItemLabelOffsetRight()} > + {this.renderCustomPieceIcons()} {this.renderInfiniteIcon()} {this.renderLoopIcon()} {this.renderOverflowTimeLabel()} diff --git a/packages/webui/src/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx b/packages/webui/src/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx index 83fd962ab0..425d8f929d 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Renderers/LocalLayerItemRenderer.tsx @@ -67,6 +67,7 @@ export class LocalLayerItemRenderer extends CustomLayerItemRenderer + {this.renderCustomPieceIcons()} {this.renderInfiniteIcon()} {this.renderOverflowTimeLabel()} diff --git a/packages/webui/src/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx b/packages/webui/src/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx index d296671f84..4492f687a6 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx @@ -215,6 +215,7 @@ export const MicSourceRenderer: React.ComponentType = withTranslation()( style={this.getItemLabelOffsetRight()} > {end} + {this.renderCustomPieceIcons()} {this.renderInfiniteIcon()} {/* this.renderOverflowTimeLabel() */} diff --git a/packages/webui/src/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx b/packages/webui/src/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx index 5c1ea029b5..9011642ed7 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Renderers/SplitsSourceRenderer.tsx @@ -115,6 +115,7 @@ export class SplitsSourceRenderer extends CustomLayerItemRenderer {end && {end}} + {this.renderCustomPieceIcons()} {this.renderInfiniteIcon()} {this.renderOverflowTimeLabel()} diff --git a/packages/webui/src/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx b/packages/webui/src/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx index f9517f13af..9127a08803 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx @@ -293,6 +293,7 @@ class VTSourceRendererBase extends CustomLayerItemRenderer {end && this.renderLoopIcon()} {end} + {this.renderCustomPieceIcons()} {this.renderInfiniteIcon()} { (!isLiveLine || part.instance.part.autoNext) && diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 3a9ab3fca4..8e59e83b99 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -1249,15 +1249,31 @@ function HeaderEditStates({ userEditOperations }: HeaderEditStatesProps) {
{userEditOperations && userEditOperations.map((operation) => { - if (operation.type !== UserEditingType.ACTION || !operation.icon || !operation.isActive) return null - - return ( - + if ( + (operation.type !== UserEditingType.ACTION && operation.type !== UserEditingType.STATE) || + (!operation.icon && !operation.iconInactive) ) + return null + + if (!operation.isActive && operation.iconInactive) { + return ( + + ) + } else if (operation.isActive && operation.icon) { + return ( + + ) + } + + return null })}
) diff --git a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx index 6a0f1327be..1bf03b1091 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -552,9 +552,12 @@ export const SourceLayerItem = (props: Readonly): JSX.Ele return { transform: 'translate(' + targetPos.toString() + 'px, 0)', + maxWidth: `${elementWidth}px`, } } - return {} + return { + maxWidth: `${elementWidth}px`, + } } const setAnchoredElsWidths = (leftAnchoredWidth: number, rightAnchoredWidth: number) => { // anchored labels will sometimes erroneously report some width. Discard if it's marginal. diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index c354c7fe26..84a548521b 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -164,7 +164,12 @@ export function PropertiesPanel(): JSX.Element {
{userEditOperations && userEditOperations.map((operation) => { - if (operation.type !== UserEditingType.ACTION || !operation.icon || !operation.isActive) return null + if ( + (operation.type !== UserEditingType.ACTION && operation.type !== UserEditingType.STATE) || + !operation.icon || + !operation.isActive + ) + return null return })}
{title}
diff --git a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx index be987562dc..033304ea92 100644 --- a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx @@ -87,6 +87,8 @@ export function UserEditOperationMenuItems({ ) case UserEditingType.SOFIE: return null + case UserEditingType.STATE: + return null default: assertNever(userEditOperation) return null