From 80ae9d8da26179ab2e09d1ddaec43875e6cb305d Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Fri, 6 Dec 2024 11:34:34 +0000 Subject: [PATCH] feat: allow editing piece properties --- .../src/documents/piece.ts | 8 +++- packages/corelib/src/dataModel/Piece.ts | 12 +++++- .../job-worker/src/blueprints/context/lib.ts | 2 + .../job-worker/src/blueprints/postProcess.ts | 3 +- .../src/client/lib/ui/pieceUiClassNames.ts | 3 ++ packages/webui/src/client/ui/RundownView.tsx | 7 ++++ .../ui/SegmentTimeline/SegmentContextMenu.tsx | 27 +++++++++++- .../ui/SegmentTimeline/SourceLayerItem.tsx | 42 ++++++++++--------- .../ui/UserEditOperations/PropertiesPanel.tsx | 27 +++++++++--- 9 files changed, 100 insertions(+), 31 deletions(-) diff --git a/packages/blueprints-integration/src/documents/piece.ts b/packages/blueprints-integration/src/documents/piece.ts index d537c2dda7..3d7f543306 100644 --- a/packages/blueprints-integration/src/documents/piece.ts +++ b/packages/blueprints-integration/src/documents/piece.ts @@ -1,4 +1,4 @@ -import { UserEditingDefinition } from '../userEditing' +import { UserEditingDefinition, UserEditingProperties } from '../userEditing' import type { IBlueprintPieceGeneric } from './pieceGeneric' /** Special types of pieces. Some are not always used in all circumstances */ @@ -35,6 +35,12 @@ export interface IBlueprintPiece * User editing definitions for this piece */ userEditOperations?: UserEditingDefinition[] + + /** + * Properties that are user editable from the properties panel in the Sofie UI, if the user saves changes to these + * it will trigger a user edit operation of type DefaultUserOperationEditProperties + */ + userEditProperties?: UserEditingProperties } export interface IBlueprintPieceDB extends IBlueprintPiece { diff --git a/packages/corelib/src/dataModel/Piece.ts b/packages/corelib/src/dataModel/Piece.ts index 88d8e95865..17a864341b 100644 --- a/packages/corelib/src/dataModel/Piece.ts +++ b/packages/corelib/src/dataModel/Piece.ts @@ -7,7 +7,7 @@ import { } from '@sofie-automation/blueprints-integration' import { ProtectedString, protectString, unprotectString } from '../protectedString' import { PieceId, RundownId, SegmentId, PartId } from './Ids' -import { CoreUserEditingDefinition } from './UserEditingDefinitions' +import { CoreUserEditingDefinition, CoreUserEditingProperties } from './UserEditingDefinitions' /** A generic list of playback availability statuses for a Piece */ export enum PieceStatusCode { @@ -50,7 +50,9 @@ export interface PieceGeneric extends Omit { /** Stringified timelineObjects */ timelineObjectsString: PieceTimelineObjectsBlob } -export interface Piece extends PieceGeneric, Omit { +export interface Piece + extends PieceGeneric, + Omit { /** * This is the id of the rundown this piece starts playing in. * Currently this is the only rundown the piece could be playing in @@ -77,6 +79,12 @@ export interface Piece extends PieceGeneric, Omit diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index fa22ee83e2..e9336b85a5 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -100,6 +100,7 @@ export const IBlueprintPieceObjectsSampleKeys = allKeysOfObject notInVision: true, abSessions: true, userEditOperations: true, + userEditProperties: true, }) // Compile a list of the keys which are allowed to be set @@ -243,6 +244,7 @@ export function convertPieceToBlueprints(piece: ReadonlyDeep extendOnHold: piece.extendOnHold, notInVision: piece.notInVision, userEditOperations: translateUserEditsToBlueprint(piece.userEditOperations), + userEditProperties: translateUserEditPropertiesToBlueprint(piece.userEditProperties), } return obj diff --git a/packages/job-worker/src/blueprints/postProcess.ts b/packages/job-worker/src/blueprints/postProcess.ts index acc2a89840..1f3103d217 100644 --- a/packages/job-worker/src/blueprints/postProcess.ts +++ b/packages/job-worker/src/blueprints/postProcess.ts @@ -44,7 +44,7 @@ import { setDefaultIdOnExpectedPackages } from '../ingest/expectedPackages' import { logger } from '../logging' import { validateTimeline } from 'superfly-timeline' import { ReadonlyDeep } from 'type-fest' -import { translateUserEditsFromBlueprint } from './context/lib' +import { translateUserEditPropertiesFromBlueprint, translateUserEditsFromBlueprint } from './context/lib' function getIdHash(docType: string, usedIds: Map, uniqueId: string): string { const count = usedIds.get(uniqueId) @@ -110,6 +110,7 @@ export function postProcessPieces( invalid: setInvalid ?? false, timelineObjectsString: EmptyPieceTimelineObjectsBlob, userEditOperations: translateUserEditsFromBlueprint(orgPiece.userEditOperations, [blueprintId]), + userEditProperties: translateUserEditPropertiesFromBlueprint(orgPiece.userEditProperties, [blueprintId]), } if (piece.pieceType !== IBlueprintPieceType.Normal) { diff --git a/packages/webui/src/client/lib/ui/pieceUiClassNames.ts b/packages/webui/src/client/lib/ui/pieceUiClassNames.ts index 4d91cb014f..a66a68e8fc 100644 --- a/packages/webui/src/client/lib/ui/pieceUiClassNames.ts +++ b/packages/webui/src/client/lib/ui/pieceUiClassNames.ts @@ -11,6 +11,7 @@ export function pieceUiClassNames( pieceInstance: PieceUi, contentStatus: ReadonlyDeep | undefined, baseClassName: string, + selected: boolean, layerType?: SourceLayerType, partId?: PartId, highlight?: boolean, @@ -54,5 +55,7 @@ export function pieceUiClassNames( disabled: pieceInstance.instance.disabled, 'invert-flash': highlight, + + 'element-selected': selected, }) } diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index e07dc11219..c9525b0faf 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -3132,9 +3132,16 @@ const RundownViewContent = translateWithTracker + selectionContext.clearAndSetSelection({ type: 'segment', elementId: id }) + } + onEditPartProps={(id) => + selectionContext.clearAndSetSelection({ type: 'part', elementId: id }) + } studioMode={this.state.studioMode} enablePlayFromAnywhere={!!studio.settings.enablePlayFromAnywhere} enableQuickLoop={!!studio.settings.enableQuickLoop} + enableUserEdits={!!studio.settings.enableUserEdits} /> diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx index 67d8e3ce7c..58c019e9e5 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -12,7 +12,7 @@ import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../lib/rundown' import { IContextMenuContext } from '../RundownView' import { PartUi, SegmentUi } from './SegmentTimelineContainer' -import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { RenderUserEditOperations } from '../UserEditOperations/RenderUserEditOperations' import * as RundownResolver from '../../lib/RundownResolver' @@ -23,11 +23,14 @@ interface IProps { onQueueNextSegment: (segmentId: SegmentId | null, e: any) => void onSetQuickLoopStart: (marker: QuickLoopMarker | null, e: any) => void onSetQuickLoopEnd: (marker: QuickLoopMarker | null, e: any) => void + onEditSegmentProps: (id: SegmentId) => void + onEditPartProps: (id: PartId) => void playlist?: DBRundownPlaylist studioMode: boolean contextMenuContext: IContextMenuContext | null enablePlayFromAnywhere: boolean enableQuickLoop: boolean + enableUserEdits: boolean } interface IState {} @@ -96,7 +99,15 @@ export const SegmentContextMenu = withTranslation()( pieceExternalId: undefined, } )} -
+ + {this.props.enableUserEdits && ( + <> +
+ this.props.onEditSegmentProps(part.instance.segmentId)}> + {t('Edit Segment Properties')} + + + )} )} {part && !part.instance.part.invalid && timecode !== null && ( @@ -177,6 +188,18 @@ export const SegmentContextMenu = withTranslation()( pieceExternalId: undefined, } )} + + {this.props.enableUserEdits && ( + <> +
+ this.props.onEditSegmentProps(part.instance.segmentId)}> + {t('Edit Segment Properties')} + + this.props.onEditPartProps(part.instance.part._id)}> + {t('Edit Part Properties')} + + + )} )} diff --git a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx index 957d56a5af..8e688fec9c 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -24,7 +24,6 @@ import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { ReadonlyDeep } from 'type-fest' import { PieceContentStatusObj } from '@sofie-automation/meteor-lib/dist/api/pieceContentStatus' import { SelectedElementsContext } from '../RundownView/SelectedElementsContext' -import classNames from 'classnames' const LEFT_RIGHT_ANCHOR_SPACER = 15 const MARGINAL_ANCHORED_WIDTH = 5 @@ -672,33 +671,38 @@ export const SourceLayerItem = withTranslation()( {(selectElementContext) => (
{ + onDoubleClick={(e) => { if (this.props.studio?.settings.enableUserEdits) { - // Until a proper data structure, the only reference is a part. - const partId = this.props.part.instance.part._id - if (!selectElementContext.isSelected(partId)) { - selectElementContext.clearAndSetSelection({ type: 'part', elementId: partId }) + const pieceId = this.props.piece.instance.piece._id + if (!selectElementContext.isSelected(pieceId)) { + selectElementContext.clearAndSetSelection({ type: 'piece', elementId: pieceId }) } else { selectElementContext.clearSelections() } + // Until a proper data structure, the only reference is a part. + // const partId = this.props.part.instance.part._id + // if (!selectElementContext.isSelected(partId)) { + // selectElementContext.clearAndSetSelection({ type: 'part', elementId: partId }) + // } else { + // selectElementContext.clearSelections() + // } } else { - this.itemDblClick + this.itemDblClick(e) } }} onMouseUp={this.itemMouseUp} diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 1c784a6a41..72605c15bf 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -16,7 +16,7 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import classNames from 'classnames' import { useTranslation } from 'react-i18next' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { Segments } from '../../collections' +import { Pieces, Segments } from '../../collections' import { UIParts } from '../Collections' import { useSelection } from '../RundownView/SelectedElementsContext' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' @@ -54,6 +54,11 @@ export function PropertiesPanel(): JSX.Element { } }, []) + const piece = useTracker(() => { + setPendingChange(undefined) + return Pieces.findOne(selectedElement?.elementId) + }, [selectedElement?.elementId]) + const part = useTracker(() => { setPendingChange(undefined) return UIParts.findOne({ _id: selectedElement?.elementId }) @@ -63,7 +68,7 @@ export function PropertiesPanel(): JSX.Element { () => Segments.findOne({ _id: part ? part.segmentId : selectedElement?.elementId }), [selectedElement?.elementId, part?.segmentId] ) - const rundownId = part ? part.rundownId : segment?.rundownId + const rundownId = piece ? piece.startRundownId : part ? part.rundownId : segment?.rundownId const handleCommitChanges = async (e: React.MouseEvent) => { if (!rundownId || !selectedElement || !pendingChange) return @@ -129,7 +134,7 @@ export function PropertiesPanel(): JSX.Element { { segmentExternalId: segment?.externalId, partExternalId: part?.externalId, - pieceExternalId: undefined, + pieceExternalId: piece?.externalId, }, { id, @@ -139,13 +144,17 @@ export function PropertiesPanel(): JSX.Element { } const userEditOperations = - selectedElement?.type === 'part' + selectedElement?.type === 'piece' + ? piece?.userEditOperations + : selectedElement?.type === 'part' ? part?.userEditOperations : selectedElement?.type === 'segment' ? segment?.userEditOperations : undefined const userEditProperties = - selectedElement?.type === 'part' + selectedElement?.type === 'piece' + ? piece?.userEditProperties + : selectedElement?.type === 'part' ? part?.userEditProperties : selectedElement?.type === 'segment' ? segment?.userEditProperties @@ -156,7 +165,13 @@ export function PropertiesPanel(): JSX.Element { } const title = - selectedElement?.type === 'part' ? part?.title : selectedElement?.type === 'segment' ? segment?.name : undefined + selectedElement?.type === 'piece' + ? piece?.name + : selectedElement?.type === 'part' + ? part?.title + : selectedElement?.type === 'segment' + ? segment?.name + : undefined return (