From 2b4c08c5d977645fe908311c3150dbb0409ba505 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 14 Oct 2024 10:19:15 +0200 Subject: [PATCH 01/90] wip: initial draft implementation of userEditPanel --- .../src/dataModel/UserEditingDefinitions.ts | 2 + .../notifications/NotificationCenterPanel.tsx | 20 +- .../client/lib/notifications/notifications.ts | 2 + .../src/client/lib/ui/icons/useredits.tsx | 12 + packages/webui/src/client/styles/main.scss | 1 + .../src/client/styles/usereditPanel.scss | 133 ++++++++ packages/webui/src/client/ui/RundownView.tsx | 8 +- .../RundownView/RundownRightHandControls.tsx | 8 + .../ui/SegmentTimeline/SegmentContextMenu.tsx | 293 ++++++++-------- .../ui/UserEditOperations/UserEditPanel.tsx | 315 ++++++++++++++++++ 10 files changed, 646 insertions(+), 148 deletions(-) create mode 100644 packages/webui/src/client/lib/ui/icons/useredits.tsx create mode 100644 packages/webui/src/client/styles/usereditPanel.scss create mode 100644 packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index 4930fbfbda..833d8bc7e7 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -21,6 +21,8 @@ export interface CoreUserEditingDefinitionForm { id: string /** Label to show to the user for this operation */ label: ITranslatableMessage + //** A json schema to add filter selections to large schema lists */ + filteringSchema?: JSONBlob /** The json schema describing the form to display */ schema: JSONBlob /** Current values to populate the form with */ diff --git a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx index d82b417ffe..3ebc1cecac 100644 --- a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx +++ b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx @@ -12,6 +12,9 @@ import update from 'immutability-helper' import { i18nTranslator } from '../../ui/i18n' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { useTranslation } from 'react-i18next' +import { UserEditPanel } from '../../ui/UserEditOperations/UserEditPanel' +import { UserEditPanelIcon } from '../ui/icons/useredits' +import { IContextMenuContext } from '../../ui/RundownView' interface IPopUpProps { id?: string @@ -152,6 +155,7 @@ interface IProps { limitCount?: number filter?: NoticeLevel + contextMenuContext: IContextMenuContext | null } interface IState { @@ -390,7 +394,7 @@ export const NotificationCenterPopUps = translateWithTracker= NoticeLevel.USEREDIT) { + return + } + return this.state.displayList ? (
( +export const NotificationCenterPanel = (props: { + limitCount?: number + filter?: NoticeLevel + contextMenuContext: IContextMenuContext | null +}): JSX.Element => (
) @@ -540,6 +554,8 @@ export function NotificationCenterPanelToggle({ ) : ((filter || 0) & (NoticeLevel.NOTIFICATION | NoticeLevel.TIP)) !== 0 ? ( + ) : ((filter || 0) & NoticeLevel.USEREDIT) !== 0 ? ( + ) : ( )} diff --git a/packages/webui/src/client/lib/notifications/notifications.ts b/packages/webui/src/client/lib/notifications/notifications.ts index e1df7695e4..6599fb4b96 100644 --- a/packages/webui/src/client/lib/notifications/notifications.ts +++ b/packages/webui/src/client/lib/notifications/notifications.ts @@ -47,6 +47,8 @@ export enum NoticeLevel { NOTIFICATION = 0b0100, // 4 /** Tips to the user */ TIP = 0b1000, // 8 + /** User editing */ + USEREDIT = 0b100000000, // 256 } /** diff --git a/packages/webui/src/client/lib/ui/icons/useredits.tsx b/packages/webui/src/client/lib/ui/icons/useredits.tsx new file mode 100644 index 0000000000..f508958e86 --- /dev/null +++ b/packages/webui/src/client/lib/ui/icons/useredits.tsx @@ -0,0 +1,12 @@ +import { JSX } from 'react' + +export function UserEditPanelIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/webui/src/client/styles/main.scss b/packages/webui/src/client/styles/main.scss index 8b3de38f60..fab2952861 100644 --- a/packages/webui/src/client/styles/main.scss +++ b/packages/webui/src/client/styles/main.scss @@ -24,6 +24,7 @@ input { @import 'modalDialog'; @import 'multiSelect'; @import 'notifications'; +@import 'usereditPanel'; @import 'organization'; @import 'overflowingContainer'; @import 'pieceStatusIcon'; diff --git a/packages/webui/src/client/styles/usereditPanel.scss b/packages/webui/src/client/styles/usereditPanel.scss new file mode 100644 index 0000000000..e63578a1ae --- /dev/null +++ b/packages/webui/src/client/styles/usereditPanel.scss @@ -0,0 +1,133 @@ +@import '_colorScheme'; +@import '_variables'; + + +.usereditpanel-pop-up { + background: #919191; + border-radius: 3px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); + margin-bottom: 3px; + + &:first-child { + margin-top: 0.9375rem; + } + + &:last-child { + margin-bottom: 0.9375rem; + } + + > .usereditpanel-pop-up__header { + background: #0A20ED; + + min-width: 2.5rem; + font-size: 1.2em; + + text-align: left; + padding-top: 0.5rem; + padding-left: 0.1rem; + + > svg { + filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.15)); + width: 1.7em; + height: 1.7em; + } + } + + > .usereditpanel-pop-up__contents { + flex: 1; + padding: 0.625rem 0.9375rem; + padding: 0.525rem 0.6375rem; + cursor: default; + + overflow-wrap: break-word; + + > h5 { + color: inherit; + margin: 0 0 0.5em; + font-weight: 500 !important; + line-height: 1.1em; + letter-spacing: 0.02em; + } + + > .usereditpanel-pop-up__actions { + display: grid; + grid-template-columns: auto auto; + grid-template-areas: 'left right'; + margin-top: 0.5em; + + .usereditpanel-pop-up__actions--button { + font-weight: 500; + letter-spacing: 0.2px; + line-height: 1.43em; + padding-left: 10px; + padding-right: 10px; + + > svg { + margin-top: -0.1em; + vertical-align: middle; + margin-right: -0.4em; + width: 1em; + height: 1em; + } + + .label { + margin-left: 10px; + line-height: inherit; + } + } + + .btn-default { + background: #d7dade; + } + + > .usereditpanel-pop-up__actions--default { + grid-area: left; + } + + > .usereditpanel-pop-up__actions--other { + text-align: right; + grid-area: right; + } + } + } + + > .usereditpanel-pop-up__dismiss { + flex: 0; + min-width: 2.2rem; + padding: 0.625rem 0.625rem 0; + + > .usereditpanel-pop-up__dismiss__button { + display: block; + background: none; + border: none; + margin: 0; + padding: 0; + font-size: 0.7rem; + + > svg { + fill: #525252; + width: 2.25em; + height: 2.25em; + vertical-align: top; + margin-top: -0.4em; + } + + &.usereditpanel-pop-up__dismiss__button--close { + > svg { + fill: #525252; + width: 1.5em; + height: 1.5em; + vertical-align: top; + margin-top: 0; + } + } + + &:hover { + > svg { + fill: #727272; + } + } + } + } +} + diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 3cca150335..187cae6630 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -2098,6 +2098,9 @@ const RundownViewContent = translateWithTracker { + this.setState({ + isNotificationsCenterOpen: NoticeLevel.USEREDIT, + }) this.setState({ contextMenuContext, }) @@ -3038,7 +3041,10 @@ const RundownViewContent = translateWithTracker {this.state.isNotificationsCenterOpen && ( - + )} , filter: NoticeLevel) => void + onToggleUserEditPanel?: (e: React.MouseEvent) => void onToggleSupportPanel?: (e: React.MouseEvent) => void onTake?: (e: React.MouseEvent) => void onStudioRouteSetSwitch?: ( @@ -153,6 +154,13 @@ export function RundownRightHandControls(props: Readonly): JSX.Element { className="type-notification" title={t('Notes')} /> + props.onToggleNotifications?.(e, NoticeLevel.USEREDIT)} + isOpen={props.isNotificationCenterOpen === NoticeLevel.USEREDIT} + filter={NoticeLevel.USEREDIT} + className="type-notification" + title={t('Notes')} + /> + + ) + case UserEditingType.FORM: + const schema = JSONBlobParse(userEditOperation.schema) + const values = clone(userEditOperation.currentValues) + return ( + <> +
+ + + + ) + default: + assertNever(userEditOperation) + return null + } + })} +
+
+ SEGMENT : {String(this.props.contextMenuContext?.segment?.name)} +
+ {this.props.contextMenuContext?.segment && + this.props.contextMenuContext?.segment?.userEditOperations?.map((userEditOperation, i) => { + switch (userEditOperation.type) { + case UserEditingType.ACTION: + return ( + <> +
+ + + ) + case UserEditingType.FORM: + return ( + <> + + + + ) + default: + assertNever(userEditOperation) + return null + } + })} + +
+ ) + } + } +) From c1e8e10c7ae2b4809b3f762883f648b92fd530cc Mon Sep 17 00:00:00 2001 From: olzzon Date: Tue, 15 Oct 2024 16:57:10 +0200 Subject: [PATCH 02/90] wip: userEditPanel grouping --- .../blueprints-integration/src/userEditing.ts | 17 +- .../src/dataModel/UserEditingDefinitions.ts | 18 +- .../job-worker/src/blueprints/context/lib.ts | 7 + .../src/client/styles/usereditPanel.scss | 67 ++- .../RundownView/RundownRightHandControls.tsx | 1 - .../RenderUserEditOperations.tsx | 49 +- .../ui/UserEditOperations/UserEditPanel.tsx | 424 ++++++++---------- 7 files changed, 329 insertions(+), 254 deletions(-) diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index b199341768..8533e614fa 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -16,8 +16,10 @@ export interface UserEditingDefinitionAction { id: string /** Label to show to the user for this operation */ label: ITranslatableMessage - /** Icon to show to when this action is 'active' */ + /** Icon to show when this action is 'active' */ svgIcon?: string + /** Icon to show when this action is 'disabled' */ + svgIconDisabled?: string /** Whether this action should be indicated as being active */ isActive?: boolean } @@ -33,6 +35,10 @@ export interface UserEditingDefinitionForm { label: ITranslatableMessage /** The json schema describing the form to display */ schema: JSONBlob + /** Used to group the schemas and filter them */ + grouping?: UserEditingGroupingType[] + /** The json schemas describing the form to display */ + schemas: Record> /** Current values to populate the form with */ currentValues: Record } @@ -43,3 +49,12 @@ export enum UserEditingType { /** Form of selections */ FORM = 'form', } + +/** + * Grouping of schemas + */ +export interface UserEditingGroupingType { + filter: string + label: string + color: string +} diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index 833d8bc7e7..c543b9f641 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -1,4 +1,9 @@ -import type { UserEditingType, JSONBlob, JSONSchema } from '@sofie-automation/blueprints-integration' +import type { + UserEditingType, + UserEditingGroupingType, + JSONBlob, + JSONSchema, +} from '@sofie-automation/blueprints-integration' import type { ITranslatableMessage } from '../TranslatableMessage' export type CoreUserEditingDefinition = CoreUserEditingDefinitionAction | CoreUserEditingDefinitionForm @@ -9,8 +14,10 @@ export interface CoreUserEditingDefinitionAction { id: string /** Label to show to the user for this operation */ label: ITranslatableMessage - /** Icon to show to when this action is 'active' */ + /** Icon to show when this action is 'active' */ svgIcon?: string + /** Icon to show when this action is 'disabled' */ + svgIconDisabled?: string /** Whether this action should be indicated as being active */ isActive?: boolean } @@ -21,10 +28,13 @@ export interface CoreUserEditingDefinitionForm { id: string /** Label to show to the user for this operation */ label: ITranslatableMessage - //** A json schema to add filter selections to large schema lists */ - filteringSchema?: JSONBlob /** The json schema describing the form to display */ schema: JSONBlob + /** Used to group the schemas and filter them */ + grouping?: UserEditingGroupingType[] + /** The json schemas describing the form to display */ + schemas: Record> + /** Current values to populate the form with */ currentValues: Record /** Translation namespaces to use when rendering this form */ diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 13cb6a4509..25ab7436c6 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -56,6 +56,7 @@ import { UserEditingDefinition, UserEditingDefinitionAction, UserEditingDefinitionForm, + UserEditingGroupingType, UserEditingType, } from '@sofie-automation/blueprints-integration/dist/userEditing' import type { PlayoutMutatablePart } from '../../playout/model/PlayoutPartInstanceModel' @@ -510,6 +511,7 @@ function translateUserEditsToBlueprint( id: userEdit.id, label: omit(userEdit.label, 'namespaces'), svgIcon: userEdit.svgIcon, + svgIconDisabled: userEdit.svgIconDisabled, isActive: userEdit.isActive, } satisfies Complete case UserEditingType.FORM: @@ -518,6 +520,8 @@ function translateUserEditsToBlueprint( id: userEdit.id, label: omit(userEdit.label, 'namespaces'), schema: clone(userEdit.schema), + grouping: clone(userEdit.grouping) as UserEditingGroupingType[] | undefined, + schemas: clone(userEdit.schemas), currentValues: clone(userEdit.currentValues), } satisfies Complete default: @@ -543,6 +547,7 @@ export function translateUserEditsFromBlueprint( id: userEdit.id, label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds), svgIcon: userEdit.svgIcon, + svgIconDisabled: userEdit.svgIconDisabled, isActive: userEdit.isActive, } satisfies Complete case UserEditingType.FORM: @@ -551,6 +556,8 @@ export function translateUserEditsFromBlueprint( id: userEdit.id, label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds), schema: clone(userEdit.schema), + grouping: clone(userEdit.grouping), + schemas: clone(userEdit.schemas), currentValues: clone(userEdit.currentValues), translationNamespaces: unprotectStringArray(blueprintIds), } satisfies Complete diff --git a/packages/webui/src/client/styles/usereditPanel.scss b/packages/webui/src/client/styles/usereditPanel.scss index e63578a1ae..100fafbfd5 100644 --- a/packages/webui/src/client/styles/usereditPanel.scss +++ b/packages/webui/src/client/styles/usereditPanel.scss @@ -3,10 +3,11 @@ .usereditpanel-pop-up { - background: #919191; - border-radius: 3px; + background: #303030; + border-radius: 1px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); - margin-bottom: 3px; + margin: 1px; + height: 100%; &:first-child { margin-top: 0.9375rem; @@ -18,6 +19,7 @@ > .usereditpanel-pop-up__header { background: #0A20ED; + color: #fff; min-width: 2.5rem; font-size: 1.2em; @@ -49,6 +51,65 @@ letter-spacing: 0.02em; } + > .usereditpanel-pop-up__groupselector { + display: flex; + flex-wrap: wrap; + margin-top: 0.5em; + margin-bottom: 0.5em; + + > .usereditpanel-pop-up__groupselector__button { + padding: 5px 5px 5px 5px; + gap: 10px; + color: #fff; + opacity: 0.3; + } + + > .usereditpanel-pop-up__groupselector__button-active { + padding: 5px 5px 5px 5px; + gap: 10px; + color: #fff; + opacity: 1; + } + + } + + > .usereditpanel-pop-up__label { + margin-left: 0.5em; + color: #fff; + } + + > .usereditpanel-pop-up_dropdownmenu { + background: #303030; + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + > .usereditpanel-pop-up__button { + margin-top: 10px; + background: #636363; + padding: 5px 5px 5px 5px; + gap: 10px; + border-radius: 5px; + border: 1px; + border: 1px solid #7F7F7F; + color: #fff; + + > svg { + margin-top: -0.1em; + vertical-align: middle; + margin-right: -0.4em; + width: 1em; + height: 1em; + } + + .label { + margin-left: 10px; + margin-right: 10px; + margin-top: 2px; + line-height: inherit; + } + } + > .usereditpanel-pop-up__actions { display: grid; grid-template-columns: auto auto; diff --git a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx index 4e47fd13d3..e82442181d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx @@ -40,7 +40,6 @@ interface IProps { isSupportPanelOpen: boolean isStudioMode: boolean onToggleNotifications?: (e: React.MouseEvent, filter: NoticeLevel) => void - onToggleUserEditPanel?: (e: React.MouseEvent) => void onToggleSupportPanel?: (e: React.MouseEvent) => void onTake?: (e: React.MouseEvent) => void onStudioRouteSetSwitch?: ( diff --git a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx index 6b0577d65d..bc64f098d1 100644 --- a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx @@ -27,22 +27,39 @@ export function RenderUserEditOperations( switch (userEditOperation.type) { case UserEditingType.ACTION: return ( - { - doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => - MeteorCall.userAction.executeUserChangeOperation(e, ts, rundownId, operationTarget, { - id: userEditOperation.id, - }) - ) - }} - > - { - // ToDo: use CSS to Style state instead of asterix - userEditOperation.isActive ? {'• '} : null - } - {translateMessage(userEditOperation.label, i18nTranslator)} - + <> + {typeof userEditOperation.isActive !== 'undefined' ? ( + { + doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => + MeteorCall.userAction.executeUserChangeOperation(e, ts, rundownId, operationTarget, { + id: userEditOperation.id, + }) + ) + }} + > + { + // ToDo: use CSS to Style state instead of asterix + userEditOperation.isActive ? {'• '} : null + } + {translateMessage(userEditOperation.label, i18nTranslator)} + + ) : ( + { + doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => + MeteorCall.userAction.executeUserChangeOperation(e, ts, rundownId, operationTarget, { + id: userEditOperation.id, + }) + ) + }} + > + {translateMessage(userEditOperation.label, i18nTranslator)} + + )} + ) case UserEditingType.FORM: return ( diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx index 77e69bbfae..85f7e2571f 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx @@ -13,6 +13,12 @@ import { JSONBlobParse, UserEditingType } from '@sofie-automation/blueprints-int import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' import { doModalDialog } from '../../lib/ModalDialog' import { SchemaFormInPlace } from '../../lib/forms/SchemaFormInPlace' +import classNames from 'classnames' +import { + CoreUserEditingDefinition, + CoreUserEditingDefinitionAction, + CoreUserEditingDefinitionForm, +} from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' /** * UserEditPanelPopUp props. @@ -22,45 +28,36 @@ interface Props { } interface State { + selectedGroup: string | undefined + selectedSource: Record dismissing: string[] dismissingTransform: string[] } -interface TrackedProps {} +interface ITrackedProps {} /** * Presentational component that displays the currents user edit operations. * @class UserEditPanel * @extends React.Component */ -export const UserEditPanel = translateWithTracker(() => { +export const UserEditPanel = translateWithTracker(() => { return {} })( - class UserEditPanelPopUp extends React.Component, State> { + class UserEditPanelPopUp extends React.Component, State> { t = i18nTranslator - private readonly DISMISS_ANIMATION_DURATION = 500 - private readonly LEAVE_ANIMATION_DURATION = 150 - constructor(props: Translated) { + constructor(props: Translated) { super(props) this.state = { + selectedGroup: undefined, + selectedSource: {}, dismissing: [], dismissingTransform: [], } } - shouldComponentUpdate(nextProps: Readonly>): boolean { - return ( - this.props.contextMenuContext?.part?.instance._id !== nextProps.contextMenuContext?.part?.instance._id || - JSON.stringify(this.props.contextMenuContext?.part?.instance.part.userEditOperations) !== - JSON.stringify(nextProps.contextMenuContext?.part?.instance.part.userEditOperations) || - this.props.contextMenuContext?.segment?._id !== nextProps.contextMenuContext?.segment?._id || - JSON.stringify(this.props.contextMenuContext?.segment?.userEditOperations) !== - JSON.stringify(nextProps.contextMenuContext?.segment?.userEditOperations) - ) - } - UNSAFE_componentWillUpdate() { Array.from(document.querySelectorAll('.usereditpanel-pop-up.is-highlighted')).forEach((element0: Element) => { const element = element0 as HTMLElement @@ -70,7 +67,7 @@ export const UserEditPanel = translateWithTracker(() }) } - componentDidUpdate(prevProps: Readonly>, prevState: State, snapshot: any) { + componentDidUpdate(prevProps: Readonly>, prevState: State, snapshot: any) { if (super.componentDidUpdate) super.componentDidUpdate(prevProps, prevState, snapshot) } @@ -87,223 +84,62 @@ export const UserEditPanel = translateWithTracker(() render(): JSX.Element { return (
-
-
-
PART : {String(this.props.contextMenuContext?.part?.instance.part.title)}
- {this.props.contextMenuContext?.part?.instance._id && - this.props.contextMenuContext?.part?.instance.part.userEditOperations?.map((userEditOperation, i) => { - this.props.contextMenuContext?.part?.instance.part.title - switch (userEditOperation.type) { - case UserEditingType.ACTION: - return ( - <> -
- - - ) - case UserEditingType.FORM: - const schema = JSONBlobParse(userEditOperation.schema) - const values = clone(userEditOperation.currentValues) - return ( - <> -
- + {this.props.contextMenuContext?.part?.instance._id && + this.props.contextMenuContext?.part?.instance.part.userEditOperations?.map((userEditOperation, i) => { + this.props.contextMenuContext?.part?.instance.part.title + switch (userEditOperation.type) { + case UserEditingType.ACTION: + return ( + - - - ) - default: - assertNever(userEditOperation) - return null - } - })} + ) + case UserEditingType.FORM: + return ( + + ) + default: + assertNever(userEditOperation) + return null + } + })} +

-
- SEGMENT : {String(this.props.contextMenuContext?.segment?.name)} +
+
+ Debug (segment) : {String(this.props.contextMenuContext?.segment?.name)} +
+ {this.props.contextMenuContext?.segment && + this.props.contextMenuContext?.segment?.userEditOperations?.map((userEditOperation, i) => { + switch (userEditOperation.type) { + case UserEditingType.ACTION: + return ( + + ) + case UserEditingType.FORM: + return ( + + ) + default: + assertNever(userEditOperation) + return null + } + })}
- {this.props.contextMenuContext?.segment && - this.props.contextMenuContext?.segment?.userEditOperations?.map((userEditOperation, i) => { - switch (userEditOperation.type) { - case UserEditingType.ACTION: - return ( - <> -
- - - ) - case UserEditingType.FORM: - return ( - <> - - - - ) - default: - assertNever(userEditOperation) - return null - } - })} (() } } ) + +function EditingTypeAction(props: { + userEditOperation: CoreUserEditingDefinitionAction + contextMenuContext: IContextMenuContext | null +}) { + return ( + <> +
+ { + doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => + MeteorCall.userAction.executeUserChangeOperation( + e, + ts, + //@ts-expect-error TODO: Fix this + this.props.contextMenuContext?.segment?.rundownId, + { + segmentExternalId: props.contextMenuContext?.segment?.externalId, + partExternalId: props.contextMenuContext?.part?.instance.part.externalId, + pieceExternalId: undefined, + }, + { + id: props.userEditOperation.id, + } + ) + ) + }} + tabIndex={0} + > +
+
+   +   +
+
+
+
+ + {translateMessage(props.userEditOperation.label, i18nTranslator)} + + + ) +} + +function EditingTypeForm(props: { + userEditOperation: CoreUserEditingDefinitionForm + contextMenuContext: IContextMenuContext | null +}) { + const [selectedGroup, setSelectedGroup] = React.useState(undefined) + const [selectedSource, setSelectedSource] = React.useState>({}) + const jsonSchema = + props.userEditOperation.schemas[ + Object.keys(props.userEditOperation.schemas || {}).filter((key) => key === selectedGroup)[0] + ] + const schema = jsonSchema ? JSONBlobParse(jsonSchema) : undefined + const values = clone(props.userEditOperation.currentValues) + return ( + <> +
+ {props.userEditOperation.grouping && + props.userEditOperation.grouping.map((group, index) => { + return ( + + ) + })} +
+ {selectedGroup && schema && ( + <> + + + + )} + + ) +} From fb54ad20abc1ea485b3b8ed403135c227c32d3df Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 16 Oct 2024 09:01:19 +0200 Subject: [PATCH 03/90] wip: userEditPanel cleanup remove schema as it's replaces by schemas --- packages/blueprints-integration/src/userEditing.ts | 2 -- packages/corelib/src/dataModel/UserEditingDefinitions.ts | 2 -- packages/job-worker/src/blueprints/context/lib.ts | 2 -- 3 files changed, 6 deletions(-) diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index 8533e614fa..83649d20d4 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -33,8 +33,6 @@ export interface UserEditingDefinitionForm { id: string /** Label to show to the user for this operation */ label: ITranslatableMessage - /** The json schema describing the form to display */ - schema: JSONBlob /** Used to group the schemas and filter them */ grouping?: UserEditingGroupingType[] /** The json schemas describing the form to display */ diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index c543b9f641..ef6e9c3c32 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -28,8 +28,6 @@ export interface CoreUserEditingDefinitionForm { id: string /** Label to show to the user for this operation */ label: ITranslatableMessage - /** The json schema describing the form to display */ - schema: JSONBlob /** Used to group the schemas and filter them */ grouping?: UserEditingGroupingType[] /** The json schemas describing the form to display */ diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 25ab7436c6..9b37e1c37e 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -519,7 +519,6 @@ function translateUserEditsToBlueprint( type: UserEditingType.FORM, id: userEdit.id, label: omit(userEdit.label, 'namespaces'), - schema: clone(userEdit.schema), grouping: clone(userEdit.grouping) as UserEditingGroupingType[] | undefined, schemas: clone(userEdit.schemas), currentValues: clone(userEdit.currentValues), @@ -555,7 +554,6 @@ export function translateUserEditsFromBlueprint( type: UserEditingType.FORM, id: userEdit.id, label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds), - schema: clone(userEdit.schema), grouping: clone(userEdit.grouping), schemas: clone(userEdit.schemas), currentValues: clone(userEdit.currentValues), From 7fd9bf1f1dcefe4e863eedafd845b6c3fadb1fd2 Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 16 Oct 2024 09:02:10 +0200 Subject: [PATCH 04/90] wip: userEditPanel styling and use { + setSelectedSource({ [selectedGroup]: e.target.value }) + }} + > + {sourceList.enum.map((source, index) => ( + + ))} + +
+ {' '} {translateMessage(props.userEditOperation.label, i18nTranslator)} ) } -function EditingTypeForm(props: { +function EditingTypeChangeSource(props: { userEditOperation: CoreUserEditingDefinitionForm contextMenuContext: IContextMenuContext | null }) { @@ -211,11 +212,17 @@ function EditingTypeForm(props: { enum: string[] tsEnumNames: string[] } + let groups: UserEditingGroupingType[] = clone(props.userEditOperation.grouping) || [] + const numberOfEmptySlots = 14 - groups.length + for (let i = 0; i < numberOfEmptySlots; i++) { + groups.push({}) + } + return ( <>
{props.userEditOperation.grouping && - props.userEditOperation.grouping.map((group, index) => { + groups.map((group, index) => { return ( @@ -236,6 +244,8 @@ function EditingTypeForm(props: {
{selectedGroup && schema && ( <> + Source: +
Date: Thu, 17 Oct 2024 07:42:48 +0200 Subject: [PATCH 07/90] wip: userEditPanel - uncomment context menu --- .../ui/SegmentTimeline/SegmentContextMenu.tsx | 293 +++++++++--------- 1 file changed, 145 insertions(+), 148 deletions(-) diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx index d7f0e10e6d..67d8e3ce7c 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -36,157 +36,154 @@ export const SegmentContextMenu = withTranslation()( constructor(props: Translated) { super(props) } + render(): JSX.Element | null { - return <> + const { t } = this.props + + if (!this.props.studioMode || !this.props.playlist || !this.props.playlist.activationId) return null + + const part = this.getPartFromContext() + const segment = this.getSegmentFromContext() + const timecode = this.getTimePosition() + const startsAt = this.getPartStartsAt() + + const isCurrentPart = + (part && this.props.playlist && part.instance._id === this.props.playlist.currentPartInfo?.partInstanceId) || + undefined + + const isSegmentEditAble = segment?._id !== this.props.playlist.queuedSegmentId + + const isPartEditAble = + isSegmentEditAble && + part?.instance._id !== this.props.playlist.currentPartInfo?.partInstanceId && + part?.instance._id !== this.props.playlist.nextPartInfo?.partInstanceId && + part?.instance._id !== this.props.playlist.previousPartInfo?.partInstanceId + + const canSetAsNext = !!this.props.playlist?.activationId + + return segment?.orphaned !== SegmentOrphanedReason.ADLIB_TESTING ? ( + + + {part && timecode === null && ( + <> + this.props.onSetNextSegment(part.instance.segmentId, e)} + disabled={isCurrentPart || !canSetAsNext} + > + Next') }}> + + {part.instance.segmentId !== this.props.playlist.queuedSegmentId ? ( + this.props.onQueueNextSegment(part.instance.segmentId, e)} + disabled={!canSetAsNext} + > + {t('Queue segment')} + + ) : ( + this.props.onQueueNextSegment(null, e)} disabled={!canSetAsNext}> + {t('Clear queued segment')} + + )} + {segment && + RenderUserEditOperations( + isSegmentEditAble, + segment.rundownId, + segment.name, + segment.userEditOperations, + { + segmentExternalId: segment?.externalId, + partExternalId: undefined, + pieceExternalId: undefined, + } + )} +
+ + )} + {part && !part.instance.part.invalid && timecode !== null && ( + <> + this.props.onSetNext(part.instance.part, e)} + disabled={!!part.instance.orphaned || !canSetAsNext} + > + Next') }}> + {startsAt !== null && + '\u00a0(' + RundownUtils.formatTimeToShortTime(Math.floor(startsAt / 1000) * 1000) + ')'} + + {startsAt !== null && part && this.props.enablePlayFromAnywhere ? ( + <> + {/* this.onSetAsNextFromHere(part.instance.part, e)} + disabled={isCurrentPart || !!part.instance.orphaned || !canSetAsNext} + > + Next Here') }}> ( + {RundownUtils.formatTimeToShortTime(Math.floor((startsAt + timecode) / 1000) * 1000)}) + */} + this.onPlayFromHere(part.instance.part, e)} + disabled={!!part.instance.orphaned || !canSetAsNext} + > + {t('Play from Here')} ( + {RundownUtils.formatTimeToShortTime(Math.floor((startsAt + timecode) / 1000) * 1000)}) + + + ) : null} + {this.props.enableQuickLoop && !RundownResolver.isLoopLocked(this.props.playlist) && ( + <> + {RundownResolver.isQuickLoopStart(part.partId, this.props.playlist) ? ( + this.props.onSetQuickLoopStart(null, e)}> + {t('Clear QuickLoop Start')} + + ) : ( + + this.props.onSetQuickLoopStart( + { type: QuickLoopMarkerType.PART, id: part.instance.part._id }, + e + ) + } + disabled={!!part.instance.orphaned || !canSetAsNext} + > + {t('Set as QuickLoop Start')} + + )} + {RundownResolver.isQuickLoopEnd(part.partId, this.props.playlist) ? ( + this.props.onSetQuickLoopEnd(null, e)}> + {t('Clear QuickLoop End')} + + ) : ( + + this.props.onSetQuickLoopEnd( + { type: QuickLoopMarkerType.PART, id: part.instance.part._id }, + e + ) + } + disabled={!!part.instance.orphaned || !canSetAsNext} + > + {t('Set as QuickLoop End')} + + )} + + )} + + {RenderUserEditOperations( + isPartEditAble, + part.instance.rundownId, + part.instance.part.title, + part.instance.part.userEditOperations, + { + segmentExternalId: segment?.externalId, + partExternalId: part.instance.part.externalId, + pieceExternalId: undefined, + } + )} + + )} +
+
+ ) : null } - // render(): JSX.Element | null { - // const { t } = this.props - - // if (!this.props.studioMode || !this.props.playlist || !this.props.playlist.activationId) return null - - // const part = this.getPartFromContext() - // const segment = this.getSegmentFromContext() - // const timecode = this.getTimePosition() - // const startsAt = this.getPartStartsAt() - - // const isCurrentPart = - // (part && this.props.playlist && part.instance._id === this.props.playlist.currentPartInfo?.partInstanceId) || - // undefined - - // const isSegmentEditAble = segment?._id !== this.props.playlist.queuedSegmentId - - // const isPartEditAble = - // isSegmentEditAble && - // part?.instance._id !== this.props.playlist.currentPartInfo?.partInstanceId && - // part?.instance._id !== this.props.playlist.nextPartInfo?.partInstanceId && - // part?.instance._id !== this.props.playlist.previousPartInfo?.partInstanceId - - // const canSetAsNext = !!this.props.playlist?.activationId - - // return segment?.orphaned !== SegmentOrphanedReason.ADLIB_TESTING ? ( - // - // - // {part && timecode === null && ( - // <> - // this.props.onSetNextSegment(part.instance.segmentId, e)} - // disabled={isCurrentPart || !canSetAsNext} - // > - // Next') }}> - // - // {part.instance.segmentId !== this.props.playlist.queuedSegmentId ? ( - // this.props.onQueueNextSegment(part.instance.segmentId, e)} - // disabled={!canSetAsNext} - // > - // {t('Queue segment')} - // - // ) : ( - // this.props.onQueueNextSegment(null, e)} disabled={!canSetAsNext}> - // {t('Clear queued segment')} - // - // )} - // {segment && - // RenderUserEditOperations( - // isSegmentEditAble, - // segment.rundownId, - // segment.name, - // segment.userEditOperations, - // { - // segmentExternalId: segment?.externalId, - // partExternalId: undefined, - // pieceExternalId: undefined, - // } - // )} - //
- // - // )} - // {part && !part.instance.part.invalid && timecode !== null && ( - // <> - // this.props.onSetNext(part.instance.part, e)} - // disabled={!!part.instance.orphaned || !canSetAsNext} - // > - // Next') }}> - // {startsAt !== null && - // '\u00a0(' + RundownUtils.formatTimeToShortTime(Math.floor(startsAt / 1000) * 1000) + ')'} - // - // {startsAt !== null && part && this.props.enablePlayFromAnywhere ? ( - // <> - // {/* this.onSetAsNextFromHere(part.instance.part, e)} - // disabled={isCurrentPart || !!part.instance.orphaned || !canSetAsNext} - // > - // Next Here') }}> ( - // {RundownUtils.formatTimeToShortTime(Math.floor((startsAt + timecode) / 1000) * 1000)}) - // */} - // this.onPlayFromHere(part.instance.part, e)} - // disabled={!!part.instance.orphaned || !canSetAsNext} - // > - // {t('Play from Here')} ( - // {RundownUtils.formatTimeToShortTime(Math.floor((startsAt + timecode) / 1000) * 1000)}) - // - // - // ) : null} - // {this.props.enableQuickLoop && !RundownResolver.isLoopLocked(this.props.playlist) && ( - // <> - // {RundownResolver.isQuickLoopStart(part.partId, this.props.playlist) ? ( - // this.props.onSetQuickLoopStart(null, e)}> - // {t('Clear QuickLoop Start')} - // - // ) : ( - // - // this.props.onSetQuickLoopStart( - // { type: QuickLoopMarkerType.PART, id: part.instance.part._id }, - // e - // ) - // } - // disabled={!!part.instance.orphaned || !canSetAsNext} - // > - // {t('Set as QuickLoop Start')} - // - // )} - // {RundownResolver.isQuickLoopEnd(part.partId, this.props.playlist) ? ( - // this.props.onSetQuickLoopEnd(null, e)}> - // {t('Clear QuickLoop End')} - // - // ) : ( - // - // this.props.onSetQuickLoopEnd( - // { type: QuickLoopMarkerType.PART, id: part.instance.part._id }, - // e - // ) - // } - // disabled={!!part.instance.orphaned || !canSetAsNext} - // > - // {t('Set as QuickLoop End')} - // - // )} - // - // )} - - // {RenderUserEditOperations( - // isPartEditAble, - // part.instance.rundownId, - // part.instance.part.title, - // part.instance.part.userEditOperations, - // { - // segmentExternalId: segment?.externalId, - // partExternalId: part.instance.part.externalId, - // pieceExternalId: undefined, - // } - // )} - // - // )} - //
- //
- // ) : null - // } - getSegmentFromContext = (): SegmentUi | null => { if (this.props.contextMenuContext && this.props.contextMenuContext.segment) { return this.props.contextMenuContext.segment From 9cbc3247387e332a0b3a956e395f19a9252b18c0 Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 17 Oct 2024 12:30:31 +0200 Subject: [PATCH 08/90] wip: UserEditPanel - Add DefaultUserOperationsTypes for revert buttons in panel --- packages/blueprints-integration/src/ingest.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/blueprints-integration/src/ingest.ts b/packages/blueprints-integration/src/ingest.ts index 057a20a9da..e4594620e6 100644 --- a/packages/blueprints-integration/src/ingest.ts +++ b/packages/blueprints-integration/src/ingest.ts @@ -124,11 +124,35 @@ export interface UserOperationTarget { pieceExternalId: string | undefined } -export type DefaultUserOperations = { - id: '__sofie-move-segment' // Future: define properly +export enum DefaultUserOperationsTypes { + REVERT_SEGMENT = 'revert-segment', + REVERT_PART = 'revert-part', + REVERT_RUNDOWN = 'revert-rundown', +} + +export interface DefaultUserOperationRevertRundown { + id: DefaultUserOperationsTypes.REVERT_RUNDOWN + payload: Record +} + +export interface DefaultUserOperationRevertSegment { + id: DefaultUserOperationsTypes.REVERT_SEGMENT payload: Record } +export interface DefaultUserOperationRevertPart { + id: DefaultUserOperationsTypes.REVERT_PART +} + +export type DefaultUserOperations = + | { + id: '__sofie-move-segment' // Future: define properly + payload: Record + } + | DefaultUserOperationRevertRundown + | DefaultUserOperationRevertSegment + | DefaultUserOperationRevertPart + export interface UserOperationChange { /** Indicate that this change is from user operations */ source: IngestChangeType.User From 0643ff3dc2b484dc9202daf7cad5ee15f49b1f65 Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 17 Oct 2024 12:55:22 +0200 Subject: [PATCH 09/90] wip: UserEditPanel - Default revert button on panel --- .../src/client/styles/usereditPanel.scss | 57 ++++++-- .../ui/UserEditOperations/UserEditPanel.tsx | 133 +++++++++--------- 2 files changed, 115 insertions(+), 75 deletions(-) diff --git a/packages/webui/src/client/styles/usereditPanel.scss b/packages/webui/src/client/styles/usereditPanel.scss index 300464cf86..8a3a8930f6 100644 --- a/packages/webui/src/client/styles/usereditPanel.scss +++ b/packages/webui/src/client/styles/usereditPanel.scss @@ -36,6 +36,48 @@ } } + > .usereditpanel-pop-up__footer { + flex: 1; + position: absolute; + bottom: 10px; + padding: 0.625rem 0.9375rem; + padding: 0.525rem 0.6375rem; + cursor: default; + + overflow-wrap: break-word; + + > hr { + margin-left: 0px; + border-color: #7F7F7F; + } + + > .usereditpanel-pop-up__button { + margin-top: 10px; + background: #636363; + padding: 5px 5px 5px 5px; + gap: 10px; + border-radius: 5px; + border: 1px; + border: 1px solid #7F7F7F; + color: #ddd; + + > svg { + margin-top: -0.1em; + vertical-align: middle; + margin-right: -0.4em; + width: 1em; + height: 1em; + } + + .label { + margin-left: 10px; + margin-right: 10px; + margin-top: 2px; + line-height: inherit; + } + } + } + > .usereditpanel-pop-up__contents { flex: 1; padding: 0.625rem 0.9375rem; @@ -44,12 +86,9 @@ overflow-wrap: break-word; - > h5 { - color: inherit; - margin: 0 0 0.5em; - font-weight: 500 !important; - line-height: 1.1em; - letter-spacing: 0.02em; + > hr { + margin-left: 0px; + border-color: #7F7F7F; } > .usereditpanel-pop-up__groupselector { @@ -62,7 +101,7 @@ width: 50px; height: 30px; border: 0px; - margin: 5px; + margin: 4px; gap: 10px; color: #ddd; opacity: 0.2; @@ -72,7 +111,7 @@ width: 50px; height: 30px; border: 0px; - margin: 5px; + margin: 4px; gap: 10px; color: #ddd; opacity: 1; @@ -91,7 +130,7 @@ > .usereditpanel-pop-up__select{ margin-top: 10px; - width: 80%; + width: 86%; height: 3em; background: #232323; margin-bottom: 0.5em; diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx index de412cb0d4..961af09275 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx @@ -1,16 +1,19 @@ import * as React from 'react' // @ts-expect-error No types available import * as VelocityReact from 'velocity-react' -import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' import { i18nTranslator } from '../i18n' import { IContextMenuContext } from '../RundownView' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { doUserAction, UserAction } from '../../lib/clientUserAction' import { MeteorCall } from '../../lib/meteorApi' import { t } from 'i18next' -import { JSONBlobParse, UserEditingGroupingType, UserEditingType } from '@sofie-automation/blueprints-integration' +import { + DefaultUserOperationsTypes, + JSONBlobParse, + UserEditingGroupingType, + UserEditingType, +} from '@sofie-automation/blueprints-integration' import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' -import { doModalDialog } from '../../lib/ModalDialog' import classNames from 'classnames' import { CoreUserEditingDefinitionAction, @@ -25,7 +28,7 @@ interface Props { contextMenuContext: IContextMenuContext | null } -export const UserEditPanel: React.FC = ({ contextMenuContext }) => { +export function UserEditPanel(props: Props) { React.useEffect(() => { return () => { Array.from(document.querySelectorAll('.usereditpanel-pop-up.is-highlighted')).forEach((element: Element) => { @@ -36,29 +39,21 @@ export const UserEditPanel: React.FC = ({ contextMenuContext }) => { } }) - const dismissAll = () => { - // Dismiss all useredits - // Implementation needed - } - - const approveAll = () => { - // Approve all useredits - // Implementation needed - } - return (
-
PART : {String(contextMenuContext?.part?.instance.part.title)}
+
+ PART : {String(props.contextMenuContext?.part?.instance.part.title)} +
- {contextMenuContext?.part?.instance._id && - contextMenuContext?.part?.instance.part.userEditOperations?.map((userEditOperation, i) => { + {props.contextMenuContext?.part?.instance._id && + props.contextMenuContext?.part?.instance.part.userEditOperations?.map((userEditOperation, i) => { switch (userEditOperation.type) { case UserEditingType.ACTION: return ( ) case UserEditingType.FORM: @@ -66,7 +61,7 @@ export const UserEditPanel: React.FC = ({ contextMenuContext }) => { ) default: @@ -74,19 +69,22 @@ export const UserEditPanel: React.FC = ({ contextMenuContext }) => { return null } })} +
-
-
Debug (segment) : {String(contextMenuContext?.segment?.name)}
- {contextMenuContext?.segment && - contextMenuContext?.segment?.userEditOperations?.map((userEditOperation, i) => { +
+ Debug (segment) : {String(props.contextMenuContext?.segment?.name)} +
+ {/* This is only until selection of segment is implemented in UI */} + {props.contextMenuContext?.segment && + props.contextMenuContext?.segment?.userEditOperations?.map((userEditOperation, i) => { switch (userEditOperation.type) { case UserEditingType.ACTION: return ( ) case UserEditingType.FORM: @@ -94,7 +92,7 @@ export const UserEditPanel: React.FC = ({ contextMenuContext }) => { ) default: @@ -103,10 +101,31 @@ export const UserEditPanel: React.FC = ({ contextMenuContext }) => { } })}
- +
+ +
) } @@ -128,7 +147,7 @@ function EditingTypeAction(props: { e, ts, //@ts-expect-error TODO: Fix this - this.props.contextMenuContext?.segment?.rundownId, + props.contextMenuContext?.segment?.rundownId, { segmentExternalId: props.contextMenuContext?.segment?.externalId, partExternalId: props.contextMenuContext?.part?.instance.part.externalId, @@ -216,6 +235,23 @@ function EditingTypeChangeSource(props: { value={selectedSource[selectedGroup] || ''} onChange={(e) => { setSelectedSource({ [selectedGroup]: e.target.value }) + doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => + MeteorCall.userAction.executeUserChangeOperation( + e, + ts, + //@ts-expect-error TODO: Fix this + props.contextMenuContext?.segment?.rundownId, + { + segmentExternalId: props.contextMenuContext?.segment?.externalId, + partExternalId: props.contextMenuContext?.part?.instance.part.externalId, + pieceExternalId: undefined, + }, + { + values: selectedSource[selectedGroup], + id: props.userEditOperation.id, + } + ) + ) }} > {sourceList.enum.map((source, index) => ( @@ -224,42 +260,7 @@ function EditingTypeChangeSource(props: { ))} -
- +
)} From 9d461295b8d476cbb832ec337749fa94e2c3396d Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 17 Oct 2024 19:27:11 +0200 Subject: [PATCH 10/90] wip: UserEditPanel - tracker on part and segment --- .../ui/UserEditOperations/UserEditPanel.tsx | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx index 961af09275..28ef327dbf 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx @@ -20,6 +20,10 @@ import { CoreUserEditingDefinitionForm, } from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' import { useTranslation } from 'react-i18next' +import { Translated, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import _ from 'underscore' +import { Segments } from '../../collections' +import { UIParts } from '../Collections' /** * UserEditPanelPopUp props. @@ -28,7 +32,10 @@ interface Props { contextMenuContext: IContextMenuContext | null } -export function UserEditPanel(props: Props) { +//function UserEditPanelBase(props: Translated) { +export function UserEditPanel(props: Translated) { + const { t } = useTranslation() + React.useEffect(() => { return () => { Array.from(document.querySelectorAll('.usereditpanel-pop-up.is-highlighted')).forEach((element: Element) => { @@ -37,16 +44,28 @@ export function UserEditPanel(props: Props) { } }) } - }) + }, []) + + const rundownId = props.contextMenuContext?.segment?.rundownId + const part = useTracker( + () => UIParts.findOne({ _id: props.contextMenuContext?.part?.instance.part?._id }), + [props.contextMenuContext?.part?.instance.part], + props.contextMenuContext?.part?.instance.part + ) + + const segment = useTracker( + () => Segments.findOne({ rundownId: rundownId, _id: props.contextMenuContext?.segment?._id }), + [props.contextMenuContext?.segment], + props.contextMenuContext?.segment + ) return (
-
- PART : {String(props.contextMenuContext?.part?.instance.part.title)} -
+
PART : {String(part?.title)}
- {props.contextMenuContext?.part?.instance._id && - props.contextMenuContext?.part?.instance.part.userEditOperations?.map((userEditOperation, i) => { + {segment && + part?._id && + part.userEditOperations?.map((userEditOperation, i) => { switch (userEditOperation.type) { case UserEditingType.ACTION: return ( @@ -72,12 +91,10 @@ export function UserEditPanel(props: Props) {
-
- Debug (segment) : {String(props.contextMenuContext?.segment?.name)} -
+
Debug (segment) : {String(segment?.name)}
{/* This is only until selection of segment is implemented in UI */} - {props.contextMenuContext?.segment && - props.contextMenuContext?.segment?.userEditOperations?.map((userEditOperation, i) => { + {segment && + segment?.userEditOperations?.map((userEditOperation, i) => { switch (userEditOperation.type) { case UserEditingType.ACTION: return ( @@ -105,22 +122,22 @@ export function UserEditPanel(props: Props) { + }} + > + REVERT PART + + ) : ( + + )}
) @@ -309,3 +368,26 @@ function getTimePosition(contextMenuContext: IContextMenuContext): number | null } return null } + +interface EditStatesIconsProps { + userEditOperations: DBSegment['userEditOperations'] +} +function EditStatesIcons({ userEditOperations }: EditStatesIconsProps) { + return ( +
+ {userEditOperations && + userEditOperations.map((operation) => { + if (operation.type === UserEditingType.FORM || !operation.svgIcon || !operation.isActive) return null + + return ( +
+ ) + })} +
+ ) +} From 5324fa35581785b932cf255822557073afbfd59f Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 21 Oct 2024 13:40:27 +0200 Subject: [PATCH 15/90] wip: UserEditPanel - add button type in UserEditingDefinitions --- .../blueprints-integration/src/userEditing.ts | 11 ++ .../src/dataModel/UserEditingDefinitions.ts | 3 + .../job-worker/src/blueprints/context/lib.ts | 2 + .../ui/UserEditOperations/UserEditPanel.tsx | 144 ++++++++++-------- 4 files changed, 96 insertions(+), 64 deletions(-) diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index 433fa00ab4..909cf58354 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -22,6 +22,8 @@ export interface UserEditingDefinitionAction { svgIconDisabled?: string /** Whether this action should be indicated as being active */ isActive?: boolean + /** Button Type */ + buttonType?: UserEditingButtonType } /** @@ -56,3 +58,12 @@ export interface UserEditingGroupingType { label?: string color?: string } + +export enum UserEditingButtonType { + /** Button */ + BUTTON = 'button', + /** Icon */ + SWITCH = 'switch', + /** Hidden */ + HIDDEN = 'hidden', +} diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index ef6e9c3c32..d028cab4b4 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -3,6 +3,7 @@ import type { UserEditingGroupingType, JSONBlob, JSONSchema, + UserEditingButtonType, } from '@sofie-automation/blueprints-integration' import type { ITranslatableMessage } from '../TranslatableMessage' @@ -20,6 +21,8 @@ export interface CoreUserEditingDefinitionAction { svgIconDisabled?: string /** Whether this action should be indicated as being active */ isActive?: boolean + //** Button Type */ + buttonType?: UserEditingButtonType } export interface CoreUserEditingDefinitionForm { diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 9b37e1c37e..98dd4509a2 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -513,6 +513,7 @@ function translateUserEditsToBlueprint( svgIcon: userEdit.svgIcon, svgIconDisabled: userEdit.svgIconDisabled, isActive: userEdit.isActive, + buttonType: userEdit.buttonType, } satisfies Complete case UserEditingType.FORM: return { @@ -548,6 +549,7 @@ export function translateUserEditsFromBlueprint( svgIcon: userEdit.svgIcon, svgIconDisabled: userEdit.svgIconDisabled, isActive: userEdit.isActive, + buttonType: userEdit.buttonType, } satisfies Complete case UserEditingType.FORM: return { diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx index 8f664fcf08..aa645e3810 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx @@ -10,6 +10,7 @@ import { t } from 'i18next' import { DefaultUserOperationsTypes, JSONBlobParse, + UserEditingButtonType, UserEditingGroupingType, UserEditingType, } from '@sofie-automation/blueprints-integration' @@ -24,7 +25,6 @@ import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import _ from 'underscore' import { Segments } from '../../collections' import { UIParts } from '../Collections' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' /** * UserEditPanelPopUp props. @@ -221,47 +221,86 @@ function EditingTypeAction(props: { userEditOperation: CoreUserEditingDefinitionAction contextMenuContext: IContextMenuContext | null }) { - return ( -
- { - doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => - MeteorCall.userAction.executeUserChangeOperation( - e, - ts, - //@ts-expect-error TODO: Fix this - props.contextMenuContext?.segment?.rundownId, - { - segmentExternalId: props.contextMenuContext?.segment?.externalId, - partExternalId: props.contextMenuContext?.part?.instance.part.externalId, - pieceExternalId: undefined, - }, - { - id: props.userEditOperation.id, - } + if (!props.userEditOperation.buttonType) return null + switch (props.userEditOperation.buttonType) { + case UserEditingButtonType.BUTTON: + return ( + + ) + case UserEditingButtonType.SWITCH: + return ( + - - - {' '} - {translateMessage(props.userEditOperation.label, i18nTranslator)} - -
- ) + ) + case UserEditingButtonType.HIDDEN || undefined: + return null + default: + assertNever(props.userEditOperation.buttonType) + return null + } } function EditingTypeChangeSource(props: { @@ -302,7 +341,7 @@ function EditingTypeChangeSource(props: { } style={{ backgroundColor: group.color }} key={index} - onClick={(e) => { + onClick={() => { setSelectedGroup(group.filter) }} disabled={!group.filter} @@ -368,26 +407,3 @@ function getTimePosition(contextMenuContext: IContextMenuContext): number | null } return null } - -interface EditStatesIconsProps { - userEditOperations: DBSegment['userEditOperations'] -} -function EditStatesIcons({ userEditOperations }: EditStatesIconsProps) { - return ( -
- {userEditOperations && - userEditOperations.map((operation) => { - if (operation.type === UserEditingType.FORM || !operation.svgIcon || !operation.isActive) return null - - return ( -
- ) - })} -
- ) -} From c8b7e79c9474b8f4e49b90db4ee41036f36c83d1 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 21 Oct 2024 14:40:01 +0200 Subject: [PATCH 16/90] wip: UserEditPanel - rename revert button to "Revert Changes" --- .../webui/src/client/ui/UserEditOperations/UserEditPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx index aa645e3810..76e95c4db2 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx @@ -185,7 +185,7 @@ export function UserEditPanel(props: Props) { ) }} > - REVERT PART + REVERT CHANGES ) : ( )} From a1fc06b7f6a71a4c9b6ffcad482fc9643691781b Mon Sep 17 00:00:00 2001 From: olzzon Date: Tue, 22 Oct 2024 09:07:17 +0200 Subject: [PATCH 17/90] wip: UserEditPanel - add support for SVG icons in grouping (e.g for split or DVE backgrounds) --- .../blueprints-integration/src/userEditing.ts | 1 + .../src/client/styles/usereditPanel.scss | 35 +++++- .../ui/UserEditOperations/UserEditPanel.tsx | 105 +++++++++--------- 3 files changed, 90 insertions(+), 51 deletions(-) diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index 909cf58354..c6e8b4c5a7 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -57,6 +57,7 @@ export interface UserEditingGroupingType { filter?: string label?: string color?: string + svgIcon?: string } export enum UserEditingButtonType { diff --git a/packages/webui/src/client/styles/usereditPanel.scss b/packages/webui/src/client/styles/usereditPanel.scss index 93b73f7f55..22a0cf1a92 100644 --- a/packages/webui/src/client/styles/usereditPanel.scss +++ b/packages/webui/src/client/styles/usereditPanel.scss @@ -116,10 +116,43 @@ border: 0px; margin: 4px; gap: 10px; - color: #ddd; + color: #fff; opacity: 1; } + > .usereditpanel-pop-up__groupselector__button-svg { + width: 50px; + height: 30px; + border: 0; + margin: 4px; + color: #ddd; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.2; // default state + + &-active { + @extend .usereditpanel-pop-up__groupselector__button-svg; + opacity: 1; + } + + > .svg-icon { + width: 100%; + height: 100%; + margin-top: -1px; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 100%; + height: 100%; + display: block; + } + } + } + } > .usereditpanel-pop-up__action { diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx index 76e95c4db2..499d92528c 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx @@ -30,6 +30,9 @@ import { UIParts } from '../Collections' * UserEditPanelPopUp props. */ interface Props { + // Currently selected context menu context + // Plan is to replace this with a more specific selection of what is selected in the UI + // When selected element for UserEditPanel has been implemented contextMenuContext: IContextMenuContext | null } @@ -163,55 +166,31 @@ export function UserEditPanel(props: Props) { )}
- {isPartSelected ? ( - - ) : ( - - )} + ) + }} + > + REVERT CHANGES +
) @@ -332,7 +311,7 @@ function EditingTypeChangeSource(props: {
{props.userEditOperation.grouping && groups.map((group, index) => { - return ( + return !group.svgIcon ? ( + ) : ( + ) })} From c76ec35510976002e728f4a2d786f20cd86b30a6 Mon Sep 17 00:00:00 2001 From: olzzon Date: Tue, 22 Oct 2024 09:32:35 +0200 Subject: [PATCH 18/90] wip: UserEditPanel - center revert button --- .../src/client/styles/usereditPanel.scss | 24 +++++++++---------- .../ui/UserEditOperations/UserEditPanel.tsx | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/webui/src/client/styles/usereditPanel.scss b/packages/webui/src/client/styles/usereditPanel.scss index 22a0cf1a92..343fa5caee 100644 --- a/packages/webui/src/client/styles/usereditPanel.scss +++ b/packages/webui/src/client/styles/usereditPanel.scss @@ -8,6 +8,7 @@ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); margin: 1px; height: 100%; + width: 87%; &:first-child { margin-top: 0.9375rem; @@ -42,17 +43,12 @@ > .usereditpanel-pop-up__footer { flex: 1; position: absolute; + left: 0; + right: 0; bottom: 10px; - padding: 0.625rem 0.9375rem; - padding: 0.525rem 0.6375rem; - cursor: default; - - overflow-wrap: break-word; - - > hr { - margin-left: 0px; - border-color: #7F7F7F; - } + display: flex; + justify-content: center; + align-items: center; > .usereditpanel-pop-up__button { margin-top: 10px; @@ -63,6 +59,8 @@ border: 1px; border: 1px solid #7F7F7F; color: #ddd; + display: block; + margin: 10px auto; > svg { margin-top: -0.1em; @@ -104,7 +102,7 @@ width: 50px; height: 30px; border: 0px; - margin: 4px; + margin: 3px; gap: 10px; color: #ddd; opacity: 0.2; @@ -114,7 +112,7 @@ width: 50px; height: 30px; border: 0px; - margin: 4px; + margin: 3px; gap: 10px; color: #fff; opacity: 1; @@ -124,7 +122,7 @@ width: 50px; height: 30px; border: 0; - margin: 4px; + margin: 3px; color: #ddd; padding: 0; display: flex; diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx index 499d92528c..cfab70369f 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx @@ -236,7 +236,7 @@ function EditingTypeAction(props: { return (
Date: Tue, 22 Oct 2024 11:20:49 +0200 Subject: [PATCH 19/90] wip: Renaming UserEditPanel to PropertiesPanel --- .../notifications/NotificationCenterPanel.tsx | 14 +++--- .../client/lib/notifications/notifications.ts | 2 +- .../src/client/lib/ui/icons/useredits.tsx | 2 +- packages/webui/src/client/styles/main.scss | 2 +- ...sereditPanel.scss => propertiesPanel.scss} | 31 ++++++------ packages/webui/src/client/ui/RundownView.tsx | 2 +- .../RundownView/RundownRightHandControls.tsx | 6 +-- ...{UserEditPanel.tsx => PropertiesPanel.tsx} | 49 +++++++++---------- 8 files changed, 54 insertions(+), 54 deletions(-) rename packages/webui/src/client/styles/{usereditPanel.scss => propertiesPanel.scss} (81%) rename packages/webui/src/client/ui/UserEditOperations/{UserEditPanel.tsx => PropertiesPanel.tsx} (88%) diff --git a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx index 4f6f12cb52..d8e7138bf8 100644 --- a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx +++ b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx @@ -12,8 +12,8 @@ import update from 'immutability-helper' import { i18nTranslator } from '../../ui/i18n' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { useTranslation } from 'react-i18next' -import { UserEditPanel } from '../../ui/UserEditOperations/UserEditPanel' -import { UserEditPanelIcon } from '../ui/icons/useredits' +import { PropertiesPanel } from '../../ui/UserEditOperations/PropertiesPanel' +import { UserEditsIcon } from '../ui/icons/useredits' import { IContextMenuContext } from '../../ui/RundownView' interface IPopUpProps { @@ -396,9 +396,9 @@ export const NotificationCenterPopUps = translateWithTracker= NoticeLevel.USEREDIT) { - return + // For PropertiesPanel ignore the filter: + if (Number(filter) >= NoticeLevel.PROPERTIES_PANEL) { + return } const notifications = this.getNotificationsToDisplay() @@ -554,8 +554,8 @@ export function NotificationCenterPanelToggle({ ) : ((filter || 0) & (NoticeLevel.NOTIFICATION | NoticeLevel.TIP)) !== 0 ? ( - ) : ((filter || 0) & NoticeLevel.USEREDIT) !== 0 ? ( - + ) : ((filter || 0) & NoticeLevel.PROPERTIES_PANEL) !== 0 ? ( + ) : ( )} diff --git a/packages/webui/src/client/lib/notifications/notifications.ts b/packages/webui/src/client/lib/notifications/notifications.ts index 6599fb4b96..62267d0d80 100644 --- a/packages/webui/src/client/lib/notifications/notifications.ts +++ b/packages/webui/src/client/lib/notifications/notifications.ts @@ -48,7 +48,7 @@ export enum NoticeLevel { /** Tips to the user */ TIP = 0b1000, // 8 /** User editing */ - USEREDIT = 0b100000000, // 256 + PROPERTIES_PANEL = 0b100000000, // 256 } /** diff --git a/packages/webui/src/client/lib/ui/icons/useredits.tsx b/packages/webui/src/client/lib/ui/icons/useredits.tsx index f508958e86..51501ebca5 100644 --- a/packages/webui/src/client/lib/ui/icons/useredits.tsx +++ b/packages/webui/src/client/lib/ui/icons/useredits.tsx @@ -1,6 +1,6 @@ import { JSX } from 'react' -export function UserEditPanelIcon(): JSX.Element { +export function UserEditsIcon(): JSX.Element { return ( .usereditpanel-pop-up__header { + > .propertiespanel-pop-up__header { background: #0A20ED; color: #ddd; @@ -40,7 +40,7 @@ } } - > .usereditpanel-pop-up__footer { + > .propertiespanel-pop-up__footer { flex: 1; position: absolute; left: 0; @@ -50,7 +50,7 @@ justify-content: center; align-items: center; - > .usereditpanel-pop-up__button { + > .propertiespanel-pop-up__button { margin-top: 10px; background: #636363; padding: 5px 5px 5px 5px; @@ -79,7 +79,7 @@ } } - > .usereditpanel-pop-up__contents { + > .propertiespanel-pop-up__contents { flex: 1; padding: 0.625rem 0.9375rem; padding: 0.525rem 0.6375rem; @@ -89,16 +89,17 @@ > hr { margin-left: 0px; + width: 100%; border-color: #7F7F7F; } - > .usereditpanel-pop-up__groupselector { + > .propertiespanel-pop-up__groupselector { display: flex; flex-wrap: wrap; margin-top: 0.5em; margin-bottom: 0.5em; - > .usereditpanel-pop-up__groupselector__button { + > .propertiespanel-pop-up__groupselector__button { width: 50px; height: 30px; border: 0px; @@ -108,7 +109,7 @@ opacity: 0.2; } - > .usereditpanel-pop-up__groupselector__button-active { + > .propertiespanel-pop-up__groupselector__button-active { width: 50px; height: 30px; border: 0px; @@ -118,7 +119,7 @@ opacity: 1; } - > .usereditpanel-pop-up__groupselector__button-svg { + > .propertiespanel-pop-up__groupselector__button-svg { width: 50px; height: 30px; border: 0; @@ -131,7 +132,7 @@ opacity: 0.2; // default state &-active { - @extend .usereditpanel-pop-up__groupselector__button-svg; + @extend .propertiespanel-pop-up__groupselector__button-svg; opacity: 1; } @@ -153,25 +154,25 @@ } - > .usereditpanel-pop-up__action { + > .propertiespanel-pop-up__action { margin-top: 15px; color: #ddd; } - > .usereditpanel-pop-up__label { + > .propertiespanel-pop-up__label { color: #ddd; } - > .usereditpanel-pop-up__select{ + > .propertiespanel-pop-up__select{ margin-top: 10px; - width: 86%; + width: 100%; height: 3em; background: #232323; margin-bottom: 0.5em; color: #ddd; } - > .usereditpanel-pop-up__button { + > .propertiespanel-pop-up__button { margin-top: 10px; background: #636363; padding: 5px 5px 5px 5px; diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 187cae6630..9c86c19fef 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -2099,7 +2099,7 @@ const RundownViewContent = translateWithTracker { this.setState({ - isNotificationsCenterOpen: NoticeLevel.USEREDIT, + isNotificationsCenterOpen: NoticeLevel.PROPERTIES_PANEL, }) this.setState({ contextMenuContext, diff --git a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx index e82442181d..0bf62b68ec 100644 --- a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx @@ -154,9 +154,9 @@ export function RundownRightHandControls(props: Readonly): JSX.Element { title={t('Notes')} /> props.onToggleNotifications?.(e, NoticeLevel.USEREDIT)} - isOpen={props.isNotificationCenterOpen === NoticeLevel.USEREDIT} - filter={NoticeLevel.USEREDIT} + onClick={(e) => props.onToggleNotifications?.(e, NoticeLevel.PROPERTIES_PANEL)} + isOpen={props.isNotificationCenterOpen === NoticeLevel.PROPERTIES_PANEL} + filter={NoticeLevel.PROPERTIES_PANEL} className="type-notification" title={t('Notes')} /> diff --git a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx similarity index 88% rename from packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx rename to packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index cfab70369f..d5097b619d 100644 --- a/packages/webui/src/client/ui/UserEditOperations/UserEditPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -27,22 +27,21 @@ import { Segments } from '../../collections' import { UIParts } from '../Collections' /** - * UserEditPanelPopUp props. + * Propertiespanel PopUp props. */ interface Props { // Currently selected context menu context // Plan is to replace this with a more specific selection of what is selected in the UI - // When selected element for UserEditPanel has been implemented + // When selected element for propertiesPanel has been implemented contextMenuContext: IContextMenuContext | null } -//function UserEditPanelBase(props: Translated) { -export function UserEditPanel(props: Props) { +export function PropertiesPanel(props: Props) { const { t } = useTranslation() React.useEffect(() => { return () => { - Array.from(document.querySelectorAll('.usereditpanel-pop-up.is-highlighted')).forEach((element: Element) => { + Array.from(document.querySelectorAll('.propertiespanel-pop-up.is-highlighted')).forEach((element: Element) => { if (element instanceof HTMLElement) { element.style.animationName = '' } @@ -66,10 +65,10 @@ export function UserEditPanel(props: Props) { const isPartSelected = getTimePosition(props.contextMenuContext || {}) return ( -
+
{isPartSelected && ( <> -
+
{part?.userEditOperations && part.userEditOperations.map((operation) => { if (operation.type === UserEditingType.FORM || !operation.svgIcon || !operation.isActive) return null @@ -86,7 +85,7 @@ export function UserEditPanel(props: Props) { })} PART : {String(part?.title)}
-
+
{isPartSelected && segment && part?._id && @@ -119,7 +118,7 @@ export function UserEditPanel(props: Props) { )} {!isPartSelected && ( <> -
+
{segment?.userEditOperations && segment.userEditOperations.map((operation) => { if (operation.type === UserEditingType.FORM || !operation.svgIcon || !operation.isActive) return null @@ -136,7 +135,7 @@ export function UserEditPanel(props: Props) { })} SEGMENT : {String(segment?.name)}
-
+
{/* This is only until selection of segment is implemented in UI */} {segment && segment?.userEditOperations?.map((userEditOperation, i) => { @@ -165,9 +164,9 @@ export function UserEditPanel(props: Props) {
)} -
+
@@ -206,7 +205,7 @@ function EditingTypeAction(props: { return (
{selectedGroup && schema && ( <> - {t('Source')}: + {t('Source')}:
))} - + */}
)} From 091af406c767b8a83eced4035dc095651df8aa23 Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 6 Nov 2024 11:44:35 +0100 Subject: [PATCH 52/90] feat: Schema selection implemented in commit button logic --- .../ui/UserEditOperations/PropertiesPanel.tsx | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 136abae9a7..bd7d027473 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -14,7 +14,6 @@ import { import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' import classNames from 'classnames' import { - CoreUserEditingDefinition, CoreUserEditingDefinitionAction, CoreUserEditingDefinitionForm, CoreUserEditingDefinitionSourceLayerForm, @@ -31,8 +30,9 @@ import { SchemaFormInPlace } from '../../lib/forms/SchemaFormInPlace' interface PendingChange { operationId: string - type: 'action' | 'form' - values?: any + userEditingType: UserEditingType + sourceLayerType?: SourceLayerType + value?: Record switchState?: boolean } @@ -80,7 +80,7 @@ export function PropertiesPanel(): JSX.Element { }, { id: change.operationId, - values: change.values, + values: change.value, } ) ) @@ -205,7 +205,6 @@ export function PropertiesPanel(): JSX.Element { SEGMENT : {String(segment?.name)}
- {/* This is only until selection of segment is implemented in UI */} {segment && segment?.userEditOperations?.map((userEditOperation, i) => { switch (userEditOperation.type) { @@ -305,7 +304,7 @@ function EditingTypeAction(props: { ...prev, { operationId: props.userEditOperation.id, - type: 'action', + userEditingType: UserEditingType.ACTION, switchState: !props.userEditOperation.isActive, }, ] @@ -399,43 +398,49 @@ function EditingTypeChangeSourceLayerSource(props: { setPendingChanges: React.Dispatch> }) { const { t } = useTranslation() - const [selectedSource, setSelectedSource] = React.useState>( - clone(props.userEditOperation.currentValues.value) - ) - const [selectedSourceGroup, setSelectedSourceGroup] = React.useState( - props.userEditOperation.currentValues.type + const [selectedSourceGroup, setSelectedSourceButton] = React.useState( + props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.sourceLayerType || + props.userEditOperation.currentValues.type ) + const [schemaValues, setSchemaValues] = React.useState({ + type: selectedSourceGroup, + value: + props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.value || + props.userEditOperation.currentValues.value, + }) - const sourceLayer = Object.values(props.userEditOperation.schemas).find( + const selectedSourceObject = Object.values(props.userEditOperation.schemas).find( (layer) => layer.sourceLayerType === selectedSourceGroup ) - const jsonSchema = sourceLayer?.schema + const jsonSchema = selectedSourceObject?.schema const schema = jsonSchema ? JSONBlobParse(jsonSchema) : undefined - const values = clone(props.userEditOperation.currentValues) const groups = clone(props.userEditOperation.schemas) || {} - const handleSourceChange = (e: React.ChangeEvent) => { - const newValue = e.target.value - if (selectedSourceGroup) { - const newSelectedSource = { [selectedSourceGroup]: newValue } - setSelectedSource(newSelectedSource) - - // Add to pending changes instead of executing immediately - props.setPendingChanges((prev) => { - const filtered = prev.filter( - (change) => !(change.operationId === props.userEditOperation.id && change.type === 'form') - ) - return [ - ...filtered, - { - operationId: props.userEditOperation.id, - type: 'form', - values: newValue, - }, - ] - }) - } + const handleSourceChange = () => { + setSchemaValues({ + type: selectedSourceGroup, + value: schemaValues.value, + }) + // Add to pending changes instead of executing immediately + props.setPendingChanges((prev) => { + const filtered = prev.filter( + (change) => + !( + change.operationId === props.userEditOperation.id && + change.userEditingType === UserEditingType.SOURCE_LAYER_FORM + ) + ) + return [ + ...filtered, + { + operationId: props.userEditOperation.id, + userEditingType: UserEditingType.SOURCE_LAYER_FORM, + sourceLayerType: selectedSourceGroup, + values: schemaValues.value, + }, + ] + }) } return ( @@ -452,7 +457,7 @@ function EditingTypeChangeSourceLayerSource(props: { style={{ backgroundColor: 'blue' }} key={index} onClick={() => { - setSelectedSourceGroup(group.sourceLayerType) + setSelectedSourceButton(group.sourceLayerType) }} > {group.sourceLayerLabel} @@ -461,11 +466,11 @@ function EditingTypeChangeSourceLayerSource(props: { })}
{schema && ( - <> +
{t('Source')}:
@@ -482,7 +487,7 @@ function EditingTypeChangeSourceLayerSource(props: { ))} */}
- +
)} ) From 4d58ce824cad96dd158de2e603f5a8cac38de9a8 Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 6 Nov 2024 12:50:21 +0100 Subject: [PATCH 53/90] feat: implement Layer colors in group selectors --- .../src/client/styles/propertiesPanel.scss | 40 ++++--------------- .../ui/UserEditOperations/PropertiesPanel.tsx | 20 +++------- 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 8d058d744b..64fb446f87 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -1,3 +1,4 @@ +@import '../styles/itemTypeColors'; @import '_colorScheme'; @import '_variables'; @@ -121,7 +122,11 @@ margin-top: 0.5em; margin-bottom: 0.5em; + + > .propertiespanel-pop-up__groupselector__button { + @include item-type-colors(); + width: 50px; height: 30px; border: 0px; @@ -132,6 +137,8 @@ } > .propertiespanel-pop-up__groupselector__button-active { + @include item-type-colors(); + width: 50px; height: 30px; border: 0px; @@ -141,39 +148,6 @@ opacity: 1; } - > .propertiespanel-pop-up__groupselector__button-svg { - width: 50px; - height: 30px; - border: 0; - margin: 3px; - color: #ddd; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - opacity: 0.2; // default state - - &-active { - @extend .propertiespanel-pop-up__groupselector__button-svg; - opacity: 1; - } - - > .svg-icon { - width: 100%; - height: 100%; - margin-top: -1px; - display: flex; - align-items: center; - justify-content: center; - - svg { - width: 100%; - height: 100%; - display: block; - } - } - } - } > .propertiespanel-pop-up__action { diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index bd7d027473..b2712023c6 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -27,6 +27,7 @@ import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SchemaFormInPlace } from '../../lib/forms/SchemaFormInPlace' +import { RundownUtils } from '../../lib/rundown' interface PendingChange { operationId: string @@ -34,6 +35,7 @@ interface PendingChange { sourceLayerType?: SourceLayerType value?: Record switchState?: boolean + cssTypeClass?: string } export function PropertiesPanel(): JSX.Element { @@ -449,12 +451,12 @@ function EditingTypeChangeSourceLayerSource(props: { {Object.values(groups).map((group, index) => { return (
)} From 4ef35fd25e95aca06a1dcc53948cc1e572b720a3 Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 7 Nov 2024 09:03:18 +0100 Subject: [PATCH 54/90] feat: only parse the selected source on useraction commit --- .../webui/src/client/styles/propertiesPanel.scss | 2 +- .../ui/UserEditOperations/PropertiesPanel.tsx | 15 +++++++++++---- .../RenderUserEditOperations.tsx | 4 +++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 64fb446f87..affe9582e5 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -42,7 +42,7 @@ } > .propertiespanel-pop-up__header { - background: #0A20ED; + background: #484848; color: #ddd; min-width: 2.5rem; diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index b2712023c6..b9614c6e0f 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -406,7 +406,7 @@ function EditingTypeChangeSourceLayerSource(props: { ) const [schemaValues, setSchemaValues] = React.useState({ type: selectedSourceGroup, - value: + values: props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.value || props.userEditOperation.currentValues.value, }) @@ -422,7 +422,7 @@ function EditingTypeChangeSourceLayerSource(props: { const handleSourceChange = () => { setSchemaValues({ type: selectedSourceGroup, - value: schemaValues.value, + values: schemaValues.values, }) // Add to pending changes instead of executing immediately props.setPendingChanges((prev) => { @@ -433,13 +433,20 @@ function EditingTypeChangeSourceLayerSource(props: { change.userEditingType === UserEditingType.SOURCE_LAYER_FORM ) ) + // Only use the key,value pair from the selected source group: + const newKey = Object.keys(schemaValues.values).find((key) => { + return props.userEditOperation.schemas[key].sourceLayerType === selectedSourceGroup + }) + if (!newKey) return filtered + const newValue = props.userEditOperation.currentValues.value[newKey] + return [ ...filtered, { operationId: props.userEditOperation.id, userEditingType: UserEditingType.SOURCE_LAYER_FORM, sourceLayerType: selectedSourceGroup, - values: schemaValues.value, + value: { [newKey]: newValue }, }, ] }) @@ -472,7 +479,7 @@ function EditingTypeChangeSourceLayerSource(props: { {t('Source')}:
diff --git a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx index 450822c410..ffc4a1b26d 100644 --- a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx @@ -71,7 +71,7 @@ export function RenderUserEditOperations( disabled={!isFormEditable} key={`${userEditOperation.id}_${i}`} onClick={(e) => { - const schema = JSONBlobParse(userEditOperation.schemas['camera']) + const schema = JSONBlobParse(userEditOperation.schema) const values = clone(userEditOperation.currentValues) // TODO: @@ -101,6 +101,8 @@ export function RenderUserEditOperations( Open Properties ) + case UserEditingType.SOURCE_LAYER_FORM: + return <> default: assertNever(userEditOperation) return null From 525cd5948eaca97aaed4889d3778d7c2854dcdf8 Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 7 Nov 2024 09:54:43 +0100 Subject: [PATCH 55/90] feat: simplify structure for userEditing source --- .../blueprints-integration/src/userEditing.ts | 5 +-- .../src/dataModel/UserEditingDefinitions.ts | 6 +-- .../ui/UserEditOperations/PropertiesPanel.tsx | 39 +++++++------------ 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index a42e821c70..96c988386d 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -58,10 +58,7 @@ export interface UserEditingDefinitionSourceLayerForm { /** The json schemas describing the form to display */ schemas: Record /** Current values to populate the form with */ - currentValues: { - type: SourceLayerType - value: Record - } + currentValues: Record } export enum UserEditingType { diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index b7db352204..0eadd6fd04 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -4,7 +4,6 @@ import type { JSONSchema, UserEditingSourceLayer, UserEditingButtonType, - SourceLayerType, } from '@sofie-automation/blueprints-integration' import type { ITranslatableMessage } from '../TranslatableMessage' @@ -61,8 +60,5 @@ export interface CoreUserEditingDefinitionSourceLayerForm { /** Translation namespaces to use when rendering this form */ translationNamespaces: string[] /** Current values to populate the form with */ - currentValues: { - type: SourceLayerType - value: Record - } + currentValues: Record } diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index b9614c6e0f..483582a9c9 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -404,26 +404,18 @@ function EditingTypeChangeSourceLayerSource(props: { props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.sourceLayerType || props.userEditOperation.currentValues.type ) - const [schemaValues, setSchemaValues] = React.useState({ - type: selectedSourceGroup, - values: - props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.value || - props.userEditOperation.currentValues.value, - }) - - const selectedSourceObject = Object.values(props.userEditOperation.schemas).find( - (layer) => layer.sourceLayerType === selectedSourceGroup + const [selectedValues, setSelectedValues] = React.useState>( + props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.value || + props.userEditOperation.currentValues.value ) - const jsonSchema = selectedSourceObject?.schema - const schema = jsonSchema ? JSONBlobParse(jsonSchema) : undefined - const groups = clone(props.userEditOperation.schemas) || {} + const jsonSchema = Object.values(props.userEditOperation.schemas).find( + (layer) => layer.sourceLayerType === selectedSourceGroup + )?.schema + const selectedGroupSchema = jsonSchema ? JSONBlobParse(jsonSchema) : undefined const handleSourceChange = () => { - setSchemaValues({ - type: selectedSourceGroup, - values: schemaValues.values, - }) + setSelectedValues(selectedValues) // Add to pending changes instead of executing immediately props.setPendingChanges((prev) => { const filtered = prev.filter( @@ -434,12 +426,11 @@ function EditingTypeChangeSourceLayerSource(props: { ) ) // Only use the key,value pair from the selected source group: - const newKey = Object.keys(schemaValues.values).find((key) => { + const newKey = Object.keys(props.userEditOperation.schemas).find((key) => { return props.userEditOperation.schemas[key].sourceLayerType === selectedSourceGroup }) if (!newKey) return filtered - const newValue = props.userEditOperation.currentValues.value[newKey] - + const newValue = selectedValues[newKey] return [ ...filtered, { @@ -455,7 +446,7 @@ function EditingTypeChangeSourceLayerSource(props: { return ( <>
- {Object.values(groups).map((group, index) => { + {Object.values(props.userEditOperation.schemas).map((group, index) => { return ( )} From 4bd57d6519fb25ec341f6a174b13178ae71e013e Mon Sep 17 00:00:00 2001 From: olzzon Date: Fri, 8 Nov 2024 10:46:20 +0100 Subject: [PATCH 61/90] wip: properties panel css dim commit+revert when not active --- .../src/client/styles/propertiesPanel.scss | 24 +++++++++++++++---- .../ui/UserEditOperations/PropertiesPanel.tsx | 18 ++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index d3479bdbe5..38d49ce920 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -90,6 +90,16 @@ top: 2px; } + &:disabled { + cursor: not-allowed; + opacity: 0.3; + + &:active { + transform: none; + top: 0; + } + } + > svg { margin-top: -0.1em; vertical-align: middle; @@ -156,19 +166,25 @@ > .propertiespanel-pop-up__action { margin-top: 15px; color: #ddd; - padding: 4px; + padding: 4px 4px; } > .properties-panel-pop-up__form { margin-top: 15px; color: #ddd; - padding: 4px; + padding: 4px 4px; + + > .properties-panel-pop-up__form__schema { + border-color: pink; + border-width: 0px; + + } } > .properties-panel-pop-up__has-been-edited { - background-color: #8a2929c0; + background-color: #ba0b9d24; border-radius: 8px; - padding: 4px + padding: 4px 4px; } diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index cf4b914ba7..29eaf5771a 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -255,7 +255,11 @@ export function PropertiesPanel(): JSX.Element { )}
-
{selectedGroupSchema && (
- {t('Source')}: -
-
+
Date: Fri, 8 Nov 2024 10:51:22 +0100 Subject: [PATCH 62/90] wip: properties panel clean up first iteration selector --- .../Parts/SegmentTimelinePart.tsx | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx b/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx index 8c6ec40c1f..1d8f31ce31 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx @@ -36,7 +36,6 @@ import { ISourceLayer } from '@sofie-automation/blueprints-integration' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { LIVE_LINE_TIME_PADDING } from '../Constants' import * as RundownResolver from '../../../lib/RundownResolver' -import { SelectedElementsContext } from '../../RundownView/SelectedElementsContext' export const SegmentTimelineLineElementId = 'rundown__segment__line__' export const SegmentTimelinePartElementId = 'rundown__segment__part__' @@ -52,7 +51,6 @@ interface IProps { playlist: DBRundownPlaylist studio: UIStudio part: PartUi - onPartClick?: (part: PartUi, e: React.MouseEvent) => void timeToPixelRatio: number onCollapseOutputToggle?: (layer: IOutputLayerUi, event: any) => void collapsedOutputs: { @@ -103,9 +101,6 @@ interface IState { highlight: boolean } export class SegmentTimelinePartClass extends React.Component>, IState> { - static contextType = SelectedElementsContext - declare context: React.ContextType - constructor(props: Readonly>>) { super(props) @@ -250,19 +245,6 @@ export class SegmentTimelinePartClass extends React.Component) => { - this.props.onPartClick?.(this.props.part, e) - - // Only toggle selection if Alt/Option is pressed - if (e.altKey && this.context) { - e.preventDefault() - this.context.clearAndSetSelection({ - type: 'partInstance', - elementId: this.props.part.instance._id, - }) - } - } - private highlightTimeout: NodeJS.Timer | undefined private onHighlight = (e: HighlightEvent) => { @@ -708,7 +690,6 @@ export class SegmentTimelinePartClass extends React.Component this.handleClick(e)} > {DEBUG_MODE && (
@@ -781,7 +762,6 @@ export class SegmentTimelinePartClass extends React.Component this.handleClick(e)} > {innerPart.invalid && !innerPart.gap ? null : ( From b4d75dd6af8751ba3dfe0d8e16fbce1fc400c3df Mon Sep 17 00:00:00 2001 From: olzzon Date: Fri, 8 Nov 2024 11:24:19 +0100 Subject: [PATCH 63/90] wip: properties panel add close upper right --- .../src/client/styles/propertiesPanel.scss | 19 +++++++++++++------ .../ui/UserEditOperations/PropertiesPanel.tsx | 7 ++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 38d49ce920..5b39820a4e 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -6,11 +6,11 @@ .properties-panel { position: fixed; - background: #fff; + background: #7b7b7b; color: #000; - top: 45px; + top: 50px; right: 0; - bottom: 20px; + bottom: 10px; width: calc(#{$notification-center-width} + 4.6875rem); z-index: 292; @@ -26,11 +26,11 @@ } .propertiespanel-pop-up { - background: #303030; + background: #252525; border-radius: 1px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); margin: 1px; - height: 100%; + height: 98.2%; width: 87%; &:first-child { @@ -41,8 +41,15 @@ margin-bottom: 0.9375rem; } + > .propertiespanel-pop-up_close { + position: absolute; + top: 25px; + right: 70px; + color: white; + } + > .propertiespanel-pop-up__header { - background: #484848; + background: #3d3d3d; color: #ddd; min-width: 2.5rem; diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 29eaf5771a..33f1ba346a 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -28,6 +28,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SchemaFormInPlace } from '../../lib/forms/SchemaFormInPlace' import { RundownUtils } from '../../lib/rundown' +import * as CoreIcon from '@nrk/core-icons/jsx' interface PendingChange { operationId: string @@ -39,7 +40,7 @@ interface PendingChange { } export function PropertiesPanel(): JSX.Element { - const { listSelectedElements } = useSelection() + const { listSelectedElements, clearSelections } = useSelection() const selectedElement = listSelectedElements()?.[0] const { t } = useTranslation() @@ -117,6 +118,10 @@ export function PropertiesPanel(): JSX.Element { return (
+
+ +
+ {rundownId && selectedElement?.type === 'part' && ( <>
From 15d77e8ef6e9eb158614e00ef91dc7909cf6c74c Mon Sep 17 00:00:00 2001 From: olzzon Date: Fri, 8 Nov 2024 12:05:43 +0100 Subject: [PATCH 64/90] fix: crash UI - remove transiongroup --- packages/webui/src/client/ui/RundownView.tsx | 42 ++++++-------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index e3dd934509..30dcacf748 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -3047,35 +3047,19 @@ const RundownViewContent = translateWithTracker )} - - {!this.state.isNotificationsCenterOpen && ( - - {(selectionContext) => { - if (selectionContext.listSelectedElements().length === 0) return null - return ( -
- -
- ) - }} -
- )} -
+ + {!this.state.isNotificationsCenterOpen && ( + + {(selectionContext) => { + if (selectionContext.listSelectedElements().length === 0) return null + return ( +
+ +
+ ) + }} +
+ )} Date: Fri, 8 Nov 2024 15:34:23 +0100 Subject: [PATCH 65/90] feat: properties panel animate in and shrink rundown view --- .../src/client/styles/propertiesPanel.scss | 8 + packages/webui/src/client/ui/RundownView.tsx | 457 +++++++++--------- .../ui/UserEditOperations/PropertiesPanel.tsx | 10 +- 3 files changed, 248 insertions(+), 227 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 5b39820a4e..e4f493bccd 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -14,6 +14,14 @@ width: calc(#{$notification-center-width} + 4.6875rem); z-index: 292; + transform: translateX(100%); + transition: transform 0.2s ease-out; + + // Add a class that will be applied when the component mounts + &.is-mounted { + transform: translateX(0%); + } + &::before { content: ' '; display: block; diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 30dcacf748..85bd01ee98 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -2966,234 +2966,238 @@ const RundownViewContent = translateWithTracker -
- {this.renderSegmentsList()} - - {this.props.matchedSegments && this.props.matchedSegments.length > 0 && getAllowStudio() && ( - - )} - - - r._id)} - firstRundown={this.props.rundowns[0]} - onActivate={this.onActivate} - studioMode={this.state.studioMode} - inActiveRundownView={this.props.inActiveRundownView} - currentRundown={this.state.currentRundown || this.props.rundowns[0]} - layout={this.state.rundownHeaderLayout} - showStyleBase={showStyleBase} - showStyleVariant={showStyleVariant} - /> - - - {this.state.studioMode && !Settings.disableBlurBorder && ( - + + { + (selectionContext) => { + return (
0, + 'rundown-view--studio-mode': this.state.studioMode, })} - >
-
- )} -
- - - - {this.renderSorensenContext()} - - - {this.state.isNotificationsCenterOpen && ( - - )} - - - {!this.state.isNotificationsCenterOpen && ( - - {(selectionContext) => { - if (selectionContext.listSelectedElements().length === 0) return null - return ( -
- -
- ) - }} -
- )} - - {this.state.isSupportPanelOpen && ( - -
- -
- - {t('Take a Snapshot')} - -
- {this.state.studioMode && ( - <> - -
- - )} - {this.state.studioMode && - this.props.casparCGPlayoutDevices && - this.props.casparCGPlayoutDevices.map((i) => ( - - -
-
- ))} -
- )} -
-
- - {this.state.studioMode && ( - - )} - - - - - - - - - {this.state.isClipTrimmerOpen && - this.state.selectedPiece && - RundownUtils.isPieceInstance(this.state.selectedPiece) && - (selectedPieceRundown === undefined ? ( - this.setState({ selectedPiece: undefined })} - title={t('Rundown not found')} - acceptText={t('Close')} + style={this.getStyle()} + onWheelCapture={this.onWheel} + onContextMenu={this.onContextMenuTop} > - {t('Rundown for piece "{{pieceLabel}}" could not be found.', { - pieceLabel: this.state.selectedPiece.instance.piece.name, - })} - - ) : ( - this.setState({ isClipTrimmerOpen: false })} - /> - ))} - - - - - - - - - {this.props.playlist && this.props.studio && this.props.showStyleBase && ( - - )} - -
- { - // USE IN CASE OF DEBUGGING EMERGENCY - /* getDeveloperMode() &&
+ {this.props.matchedSegments && this.props.matchedSegments.length > 0 && getAllowStudio() && ( + + )} + + + r._id)} + firstRundown={this.props.rundowns[0]} + onActivate={this.onActivate} + studioMode={this.state.studioMode} + inActiveRundownView={this.props.inActiveRundownView} + currentRundown={this.state.currentRundown || this.props.rundowns[0]} + layout={this.state.rundownHeaderLayout} + showStyleBase={showStyleBase} + showStyleVariant={showStyleVariant} + /> + + + {this.state.studioMode && !Settings.disableBlurBorder && ( + +
+
+ )} +
+ + + + {this.renderSorensenContext()} + + + {this.state.isNotificationsCenterOpen && ( + + )} + + + {!this.state.isNotificationsCenterOpen && + selectionContext.listSelectedElements().length > 0 && ( +
+ +
+ )} + + {this.state.isSupportPanelOpen && ( + +
+ +
+ + {t('Take a Snapshot')} + +
+ {this.state.studioMode && ( + <> + +
+ + )} + {this.state.studioMode && + this.props.casparCGPlayoutDevices && + this.props.casparCGPlayoutDevices.map((i) => ( + + +
+
+ ))} +
+ )} +
+
+ + {this.state.studioMode && ( + + )} + + + + + + + + + {this.state.isClipTrimmerOpen && + this.state.selectedPiece && + RundownUtils.isPieceInstance(this.state.selectedPiece) && + (selectedPieceRundown === undefined ? ( + this.setState({ selectedPiece: undefined })} + title={t('Rundown not found')} + acceptText={t('Close')} + > + {t('Rundown for piece "{{pieceLabel}}" could not be found.', { + pieceLabel: this.state.selectedPiece.instance.piece.name, + })} + + ) : ( + this.setState({ isClipTrimmerOpen: false })} + /> + ))} + + + + + + + + + {this.props.playlist && this.props.studio && this.props.showStyleBase && ( + + )} + +
+ ) + } + // USE IN CASE OF DEBUGGING EMERGENCY + /* getDeveloperMode() &&
*/ - } + } +
diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 33f1ba346a..dc827274b5 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -47,6 +47,14 @@ export function PropertiesPanel(): JSX.Element { const [pendingChanges, setPendingChanges] = React.useState([]) const hasPendingChanges = pendingChanges.length > 0 + const [isAnimatedIn, setIsAnimatedIn] = React.useState(false) + React.useEffect(() => { + const timer = setTimeout(() => { + setIsAnimatedIn(true) + }, 10) + return () => clearTimeout(timer) + }, []) + React.useEffect(() => { return () => { Array.from(document.querySelectorAll('.propertiespanel-pop-up.is-highlighted')).forEach((element: Element) => { @@ -116,7 +124,7 @@ export function PropertiesPanel(): JSX.Element { } return ( -
+
From 8666da3a7fa8d07b92fd4466557f471177d58f1d Mon Sep 17 00:00:00 2001 From: olzzon Date: Fri, 8 Nov 2024 15:36:35 +0100 Subject: [PATCH 66/90] fix: cleanup using notificationpanel for properties panel --- packages/webui/src/client/ui/RundownView.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 85bd01ee98..985402f2cd 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -2100,9 +2100,6 @@ const RundownViewContent = translateWithTracker { - this.setState({ - isNotificationsCenterOpen: NoticeLevel.PROPERTIES_PANEL, - }) this.setState({ contextMenuContext, }) From 631ef972b694f191ff1fd211e0050182ae3fd8a8 Mon Sep 17 00:00:00 2001 From: olzzon Date: Fri, 8 Nov 2024 18:57:39 +0100 Subject: [PATCH 67/90] wip: properties panel, rightbar icon color --- packages/webui/src/client/lib/ui/icons/useredits.tsx | 6 +----- .../src/client/ui/RundownView/RundownRightHandControls.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/lib/ui/icons/useredits.tsx b/packages/webui/src/client/lib/ui/icons/useredits.tsx index ae61ea9a6b..79d913d1c9 100644 --- a/packages/webui/src/client/lib/ui/icons/useredits.tsx +++ b/packages/webui/src/client/lib/ui/icons/useredits.tsx @@ -16,11 +16,7 @@ export function UserEditsCloseIcon(): JSX.Element { - ) diff --git a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx index 8b6bd83f7b..9bec7803a2 100644 --- a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx @@ -158,7 +158,12 @@ export function RundownRightHandControls(props: Readonly): JSX.Element { /> {(context) => ( - )} From ad7854906515020e8caab8e8f9ee387a19d59820 Mon Sep 17 00:00:00 2001 From: olzzon Date: Sat, 9 Nov 2024 09:31:56 +0100 Subject: [PATCH 68/90] fix: re-run only if part.segmentId has changed --- .../webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index dc827274b5..7043c4b7d5 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -72,7 +72,7 @@ export function PropertiesPanel(): JSX.Element { const segment: DBSegment | undefined = useTracker( () => Segments.findOne({ _id: part ? part.segmentId : selectedElement?.elementId }), - [selectedElement?.elementId, part] + [selectedElement?.elementId, part?.segmentId] ) const rundownId = part ? part.rundownId : segment?.rundownId @@ -429,7 +429,6 @@ function EditingTypeChangeSourceLayerSource(props: { pendingChanges: PendingChange[] setPendingChanges: React.Dispatch> }) { - const { t } = useTranslation() const [selectedSourceGroup, setSelectedSourceButton] = React.useState( props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.sourceLayerType || props.userEditOperation.currentValues.type From d3e0ca7c8749313c686752912328d1f60e166846 Mon Sep 17 00:00:00 2001 From: olzzon Date: Sat, 9 Nov 2024 09:32:10 +0100 Subject: [PATCH 69/90] wip: tests for properties panel --- packages/webui/package.json | 2 + .../webui/src/client/__tests__/jest-setup.cjs | 1 + .../__tests__/PropertiesPanel.test.tsx | 370 ++++++++++++++++++ packages/yarn.lock | 64 +++ 4 files changed, 437 insertions(+) create mode 100644 packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx diff --git a/packages/webui/package.json b/packages/webui/package.json index 14dd16f95d..5f9359c6e3 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -45,6 +45,7 @@ "@sofie-automation/meteor-lib": "1.52.0-in-development", "@sofie-automation/shared-lib": "1.52.0-in-development", "@sofie-automation/sorensen": "^1.4.3", + "@testing-library/user-event": "^14.5.2", "@types/sinon": "^10.0.20", "classnames": "^2.5.1", "cubic-spline": "^3.0.3", @@ -85,6 +86,7 @@ "devDependencies": { "@babel/preset-env": "^7.24.8", "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@types/classnames": "^2.3.1", "@types/deep-extend": "^0.6.2", diff --git a/packages/webui/src/client/__tests__/jest-setup.cjs b/packages/webui/src/client/__tests__/jest-setup.cjs index bb36d10e75..8856ce9d6a 100644 --- a/packages/webui/src/client/__tests__/jest-setup.cjs +++ b/packages/webui/src/client/__tests__/jest-setup.cjs @@ -1,4 +1,5 @@ /* eslint-disable node/no-unpublished-require */ +require('@testing-library/jest-dom') // used by code creating XML with the DOM API to return an XML string global.XMLSerializer = require('@xmldom/xmldom').XMLSerializer diff --git a/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx new file mode 100644 index 0000000000..3e70ccb829 --- /dev/null +++ b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx @@ -0,0 +1,370 @@ +// Mock the ReactiveDataHelper: +jest.mock('../../../lib/reactiveData/ReactiveDataHelper', () => { + class MockReactiveDataHelper { + protected _subs: Array<{ stop: () => void }> = [] + + protected subscribe() { + const sub = { stop: jest.fn() } + this._subs.push(sub) + return sub + } + + protected autorun(f: () => void) { + f() + return { stop: jest.fn() } + } + + destroy() { + this._subs.forEach((sub) => sub.stop()) + this._subs = [] + } + } + + class MockWithManagedTracker extends MockReactiveDataHelper { + constructor() { + super() + } + } + + return { + __esModule: true, + WithManagedTracker: MockWithManagedTracker, + meteorSubscribe: jest.fn().mockReturnValue({ + stop: jest.fn(), + }), + } +}) + +jest.mock('i18next', () => ({ + use: jest.fn().mockReturnThis(), + init: jest.fn().mockImplementation(() => Promise.resolve()), + t: (key: string) => key, + changeLanguage: jest.fn().mockImplementation(() => Promise.resolve()), + language: 'en', + exists: jest.fn(), + on: jest.fn(), + off: jest.fn(), + options: {}, +})) + +// React-i18next with Promise support +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { + changeLanguage: jest.fn().mockImplementation(() => Promise.resolve()), + language: 'en', + exists: jest.fn(), + use: jest.fn().mockReturnThis(), + init: jest.fn().mockImplementation(() => Promise.resolve()), + on: jest.fn(), + off: jest.fn(), + options: {}, + }, + }), + initReactI18next: { + type: '3rdParty', + init: jest.fn(), + }, +})) + +import React from 'react' +// eslint-disable-next-line node/no-unpublished-import +import { renderHook, act, render, screen, RenderResult } from '@testing-library/react' +// eslint-disable-next-line node/no-unpublished-import +import '@testing-library/jest-dom' +import { MeteorCall } from '../../../lib/meteorApi' +import { TFunction } from 'i18next' + +import userEvent from '@testing-library/user-event' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { UIParts } from '../../Collections' +import { Segments } from '../../../../client/collections' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { UserEditingType, UserEditingButtonType } from '@sofie-automation/blueprints-integration' +import { SelectedElementProvider, useSelection } from '../../RundownView/SelectedElementsContext' +import { MongoMock } from '../../../../__mocks__/mongo' +import { PropertiesPanel } from '../PropertiesPanel' +import { UserAction } from '../../../lib/clientUserAction' + +const mockSegmentsCollection = MongoMock.getInnerMockCollection(Segments) +const mockPartsCollection = MongoMock.getInnerMockCollection(UIParts) + +// Mock Client User Action: +jest.mock('../../../lib/clientUserAction', () => ({ + doUserAction: jest.fn((_t: TFunction, e: unknown, _action: UserAction, callback: Function) => + callback(e, Date.now()) + ), +})) + +// Mock Userchange Operation: +jest.mock('../../../lib/meteorApi', () => ({ + __esModule: true, + MeteorCall: { + userAction: { + executeUserChangeOperation: jest.fn(), + }, + }, +})) + +// Mock SchemaFormInPlace Component +jest.mock('../../../lib/forms/SchemaFormInPlace', () => ({ + SchemaFormInPlace: () =>
Schema Form
, +})) + +describe('PropertiesPanel', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ) + + beforeEach(() => { + mockSegmentsCollection.remove({}) + mockPartsCollection.remove({}) + jest.clearAllMocks() + }) + + const createMockSegment = (id: string): DBSegment => ({ + _id: protectString(id), + _rank: 1, + name: `Segment ${id}`, + rundownId: protectString('rundown1'), + externalId: `ext_${id}`, + userEditOperations: [ + { + id: 'operation1', + label: { key: 'TEST_LABEL' }, + type: UserEditingType.ACTION, + buttonType: UserEditingButtonType.SWITCH, + isActive: false, + }, + ], + }) + + const createMockPart = (id: string, segmentId: string): DBPart => ({ + _id: protectString(id), + _rank: 1, + expectedDurationWithTransition: 0, + title: `Part ${id}`, + rundownId: protectString('rundown1'), + segmentId: protectString(segmentId), + externalId: `ext_${id}`, + userEditOperations: [ + { + id: 'operation2', + label: { key: 'TEST_PART_LABEL' }, + type: UserEditingType.ACTION, + buttonType: UserEditingButtonType.BUTTON, + isActive: true, + }, + ], + }) + + test('renders empty when no element selected', () => { + const { container } = render(, { wrapper }) + expect(container.querySelector('.properties-panel')).toBeTruthy() + expect(container.querySelector('.propertiespanel-pop-up__contents')).toBeFalsy() + }) + + test('renders segment properties when segment is selected', async () => { + const mockSegment = createMockSegment('segment1') + mockSegmentsCollection.insert(mockSegment) + + // Create a custom wrapper that includes both providers + const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} + ) + + // Render both the hook and component in the same provider tree + const { result } = renderHook(() => useSelection(), { wrapper: TestWrapper }) + let rendered: RenderResult + + await act(async () => { + rendered = render(, { wrapper: TestWrapper }) + }) + + // Update selection + await act(async () => { + result.current.clearAndSetSelection({ + type: 'segment', + elementId: mockSegment._id, + }) + }) + //@ts-expect-error error because avoiding an undefined type + if (!rendered) throw new Error('Component not rendered') + + // Force a rerender + await act(async () => { + rendered.rerender() + }) + + // Wait for the header element to appear + await screen.findByText('SEGMENT : Segment segment1') + + const header = rendered.container.querySelector('.propertiespanel-pop-up__header') + const switchButton = rendered.container.querySelector('.propertiespanel-pop-up__switchbutton') + + expect(header).toHaveTextContent('SEGMENT : Segment segment1') + expect(switchButton).toBeTruthy() + }) + + test('renders part properties when part is selected', async () => { + const mockSegment = createMockSegment('segment1') + const mockPart = createMockPart('part1', String(mockSegment._id)) + + mockSegmentsCollection.insert(mockSegment) + mockPartsCollection.insert(mockPart) + + // Create a custom wrapper that includes both providers + const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} + ) + + // Render both the hook and component in the same provider tree + const { result } = renderHook(() => useSelection(), { wrapper: TestWrapper }) + let rendered: RenderResult + + await act(async () => { + rendered = render(, { wrapper: TestWrapper }) + }) + + // Update selection + await act(async () => { + result.current.clearAndSetSelection({ + type: 'part', + elementId: mockPart._id, + }) + }) + + //@ts-expect-error error because avoiding an undefined type + if (!rendered) throw new Error('Component not rendered') + + // Force a rerender + await act(async () => { + rendered.rerender() + }) + + // Wait for the header element to appear + await screen.findByText('PART : Part part1') + + const header = rendered.container.querySelector('.propertiespanel-pop-up__header') + const button = rendered.container.querySelector('.propertiespanel-pop-up__button') + + expect(header).toHaveTextContent('PART : Part part1') + expect(button).toBeTruthy() + }) + + test('handles user edit operations for segments', async () => { + const mockSegment = createMockSegment('segment1') + mockSegmentsCollection.insert(mockSegment) + + // First render the selection hook + const { result } = renderHook(() => useSelection(), { wrapper }) + + // Then render the properties panel + const { container } = render(, { wrapper }) + + // Update selection using the hook result + act(() => { + result.current.clearAndSetSelection({ + type: 'segment', + elementId: mockSegment._id, + }) + }) + + const switchButton = container.querySelector('.propertiespanel-pop-up__switchbutton') + expect(switchButton).toBeTruthy() + + // Toggle the switch + await userEvent.click(switchButton!) + + // Check if commit button is enabled + const commitButton = screen.getByText('COMMIT CHANGES') + expect(commitButton).toBeEnabled() + + // Commit changes + await userEvent.click(commitButton) + + expect(MeteorCall.userAction.executeUserChangeOperation).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + protectString('rundown1'), + { + segmentExternalId: mockSegment.externalId, + partExternalId: undefined, + pieceExternalId: undefined, + }, + { + id: 'operation1', + values: undefined, + } + ) + }) + + test('handles revert changes', async () => { + const mockSegment = createMockSegment('segment1') + mockSegmentsCollection.insert(mockSegment) + + // First render the selection hook + const { result } = renderHook(() => useSelection(), { wrapper }) + + // Then render the properties panel + const { container } = render(, { wrapper }) + + // Update selection using the hook result + act(() => { + result.current.clearAndSetSelection({ + type: 'segment', + elementId: mockSegment._id, + }) + }) + + // Make a change + const switchButton = container.querySelector('.propertiespanel-pop-up__switchbutton') + await userEvent.click(switchButton!) + + // Click revert button + const revertButton = screen.getByText('REVERT CHANGES') + await userEvent.click(revertButton) + + expect(MeteorCall.userAction.executeUserChangeOperation).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + protectString('rundown1'), + { + segmentExternalId: mockSegment.externalId, + partExternalId: undefined, + pieceExternalId: undefined, + }, + { + id: 'REVERT_SEGMENT', + } + ) + }) + + test('closes panel when close button is clicked', async () => { + const mockSegment = createMockSegment('segment1') + mockSegmentsCollection.insert(mockSegment) + + // First render the selection hook + const { result } = renderHook(() => useSelection(), { wrapper }) + + // Then render the properties panel + const { container } = render(, { wrapper }) + + // Update selection using the hook result + act(() => { + result.current.clearAndSetSelection({ + type: 'segment', + elementId: mockSegment._id, + }) + }) + + const closeButton = container.querySelector('.propertiespanel-pop-up_close') + expect(closeButton).toBeTruthy() + + await userEvent.click(closeButton!) + + expect(container.querySelector('.propertiespanel-pop-up__contents')).toBeFalsy() + }) +}) diff --git a/packages/yarn.lock b/packages/yarn.lock index 3b52f39466..538e14a02a 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -23,6 +23,13 @@ __metadata: languageName: node linkType: hard +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.0 + resolution: "@adobe/css-tools@npm:4.4.0" + checksum: 1f08fb49bf17fc7f2d1a86d3e739f29ca80063d28168307f1b0a962ef37501c5667271f6771966578897f2e94e43c4770fd802728a6e6495b812da54112d506a + languageName: node + linkType: hard + "@algolia/autocomplete-core@npm:1.9.3": version: 1.9.3 resolution: "@algolia/autocomplete-core@npm:1.9.3" @@ -5315,7 +5322,9 @@ __metadata: "@sofie-automation/shared-lib": 1.52.0-in-development "@sofie-automation/sorensen": ^1.4.3 "@testing-library/dom": ^10.4.0 + "@testing-library/jest-dom": ^6.6.3 "@testing-library/react": ^16.0.1 + "@testing-library/user-event": ^14.5.2 "@types/classnames": ^2.3.1 "@types/deep-extend": ^0.6.2 "@types/react": ^18.3.3 @@ -5915,6 +5924,21 @@ __metadata: languageName: node linkType: hard +"@testing-library/jest-dom@npm:^6.6.3": + version: 6.6.3 + resolution: "@testing-library/jest-dom@npm:6.6.3" + dependencies: + "@adobe/css-tools": ^4.4.0 + aria-query: ^5.0.0 + chalk: ^3.0.0 + css.escape: ^1.5.1 + dom-accessibility-api: ^0.6.3 + lodash: ^4.17.21 + redent: ^3.0.0 + checksum: c1dc4260b05309a0084416639006cd105849acc5b102bef682a3b19bd6fce07ff6762085fc7f2599546c995a2fc66fdb1d70e50e22a634a0098524056cc9e511 + languageName: node + linkType: hard + "@testing-library/react@npm:^16.0.1": version: 16.0.1 resolution: "@testing-library/react@npm:16.0.1" @@ -5935,6 +5959,15 @@ __metadata: languageName: node linkType: hard +"@testing-library/user-event@npm:^14.5.2": + version: 14.5.2 + resolution: "@testing-library/user-event@npm:14.5.2" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: d76937dffcf0082fbf3bb89eb2b81a31bf5448048dd61c33928c5f10e33a58e035321d39145cefd469bb5a499c68a5b4086b22f1a44e3e7c7e817dc5f6782867 + languageName: node + linkType: hard + "@tokenizer/token@npm:^0.3.0": version: 0.3.0 resolution: "@tokenizer/token@npm:0.3.0" @@ -7848,6 +7881,13 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:^5.0.0": + version: 5.3.2 + resolution: "aria-query@npm:5.3.2" + checksum: d971175c85c10df0f6d14adfe6f1292409196114ab3c62f238e208b53103686f46cc70695a4f775b73bc65f6a09b6a092fd963c4f3a5a7d690c8fc5094925717 + languageName: node + linkType: hard + "array-buffer-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "array-buffer-byte-length@npm:1.0.1" @@ -9198,6 +9238,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -10475,6 +10525,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: f6d38088d870a961794a2580b2b2af1027731bb43261cfdce14f19238a88664b351cc8978abc20f06cc6bbde725699dec8deb6fe9816b139fc3f2af28719e774 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -11614,6 +11671,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"dom-accessibility-api@npm:^0.6.3": + version: 0.6.3 + resolution: "dom-accessibility-api@npm:0.6.3" + checksum: c325b5144bb406df23f4affecffc117dbaec9af03daad9ee6b510c5be647b14d28ef0a4ea5ca06d696d8ab40bb777e5fed98b985976fdef9d8790178fa1d573f + languageName: node + linkType: hard + "dom-converter@npm:^0.2.0": version: 0.2.0 resolution: "dom-converter@npm:0.2.0" From 5fe974f9591dff04919adfb6909ab3bf8917f823 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 11 Nov 2024 09:20:17 +0100 Subject: [PATCH 70/90] wip: properties panel tests - useFaketimers and implement mock useTracker() --- packages/webui/jest.config.cjs | 6 +- .../__tests__/PropertiesPanel.test.tsx | 169 ++++++++++-------- 2 files changed, 96 insertions(+), 79 deletions(-) diff --git a/packages/webui/jest.config.cjs b/packages/webui/jest.config.cjs index e193bd1aad..6458660efb 100644 --- a/packages/webui/jest.config.cjs +++ b/packages/webui/jest.config.cjs @@ -1,5 +1,9 @@ module.exports = { - setupFilesAfterEnv: ['./src/__mocks__/_setupMocks.ts', '/src/client/__tests__/jest-setup.cjs'], + setupFilesAfterEnv: [ + './src/__mocks__/_setupMocks.ts', + '/src/client/__tests__/jest-setup.cjs', + '@testing-library/jest-dom', + ], globals: {}, moduleFileExtensions: ['js', 'ts', 'tsx'], moduleNameMapper: { diff --git a/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx index 3e70ccb829..7ca0d5e5f1 100644 --- a/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx @@ -1,3 +1,30 @@ +jest.mock( + '../../../../__mocks__/tracker', + () => ({ + setup: () => ({ + Tracker: { + autorun: jest.fn((fn) => { + fn() + return { + stop: jest.fn(), + } + }), + nonreactive: jest.fn((fn) => fn()), + active: false, + currentComputation: null, + Dependency: jest.fn().mockImplementation(() => ({ + depend: jest.fn(), + changed: jest.fn(), + hasDependents: jest.fn(), + })), + }, + }), + }), + { + virtual: true, + } +) + // Mock the ReactiveDataHelper: jest.mock('../../../lib/reactiveData/ReactiveDataHelper', () => { class MockReactiveDataHelper { @@ -70,7 +97,7 @@ jest.mock('react-i18next', () => ({ import React from 'react' // eslint-disable-next-line node/no-unpublished-import -import { renderHook, act, render, screen, RenderResult } from '@testing-library/react' +import { renderHook, act, render, screen, waitFor } from '@testing-library/react' // eslint-disable-next-line node/no-unpublished-import import '@testing-library/jest-dom' import { MeteorCall } from '../../../lib/meteorApi' @@ -122,6 +149,11 @@ describe('PropertiesPanel', () => { mockSegmentsCollection.remove({}) mockPartsCollection.remove({}) jest.clearAllMocks() + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() }) const createMockSegment = (id: string): DBSegment => ({ @@ -133,12 +165,14 @@ describe('PropertiesPanel', () => { userEditOperations: [ { id: 'operation1', - label: { key: 'TEST_LABEL' }, + label: { key: 'TEST_LABEL', namespaces: ['blueprint_main-showstyle'] }, type: UserEditingType.ACTION, buttonType: UserEditingButtonType.SWITCH, isActive: false, + svgIcon: '', }, ], + isHidden: false, }) const createMockPart = (id: string, segmentId: string): DBPart => ({ @@ -152,7 +186,7 @@ describe('PropertiesPanel', () => { userEditOperations: [ { id: 'operation2', - label: { key: 'TEST_PART_LABEL' }, + label: { key: 'TEST_PART_LABEL', namespaces: ['blueprint_main-showstyle'] }, type: UserEditingType.ACTION, buttonType: UserEditingButtonType.BUTTON, isActive: true, @@ -168,44 +202,39 @@ describe('PropertiesPanel', () => { test('renders segment properties when segment is selected', async () => { const mockSegment = createMockSegment('segment1') - mockSegmentsCollection.insert(mockSegment) - // Create a custom wrapper that includes both providers - const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - {children} - ) + mockSegmentsCollection.insert(mockSegment) - // Render both the hook and component in the same provider tree - const { result } = renderHook(() => useSelection(), { wrapper: TestWrapper }) - let rendered: RenderResult + expect(mockSegmentsCollection.findOne({ _id: mockSegment._id })).toBeTruthy() - await act(async () => { - rendered = render(, { wrapper: TestWrapper }) - }) + const { result } = renderHook(() => useSelection(), { wrapper }) - // Update selection + // Update selection and wait for component to update await act(async () => { result.current.clearAndSetSelection({ type: 'segment', elementId: mockSegment._id, }) }) - //@ts-expect-error error because avoiding an undefined type - if (!rendered) throw new Error('Component not rendered') - // Force a rerender + // Open component after segment is selected (as used in rundownview) + const { container } = render(, { wrapper }) + await act(async () => { - rendered.rerender() + jest.advanceTimersByTime(100) }) - // Wait for the header element to appear - await screen.findByText('SEGMENT : Segment segment1') - - const header = rendered.container.querySelector('.propertiespanel-pop-up__header') - const switchButton = rendered.container.querySelector('.propertiespanel-pop-up__switchbutton') + console.log('result', result.current.listSelectedElements()) + // Use findByTestId instead of querySelector + await waitFor( + () => { + expect(screen.getByText(`SEGMENT : ${mockSegment.name}`)).toBeInTheDocument() + }, + { timeout: 1000 } + ) - expect(header).toHaveTextContent('SEGMENT : Segment segment1') - expect(switchButton).toBeTruthy() + const button = container.querySelector('.propertiespanel-pop-up__button') + expect(button).toBeInTheDocument() }) test('renders part properties when part is selected', async () => { @@ -215,75 +244,59 @@ describe('PropertiesPanel', () => { mockSegmentsCollection.insert(mockSegment) mockPartsCollection.insert(mockPart) - // Create a custom wrapper that includes both providers - const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - {children} - ) - - // Render both the hook and component in the same provider tree - const { result } = renderHook(() => useSelection(), { wrapper: TestWrapper }) - let rendered: RenderResult - - await act(async () => { - rendered = render(, { wrapper: TestWrapper }) - }) + const { result } = renderHook(() => useSelection(), { wrapper }) - // Update selection await act(async () => { result.current.clearAndSetSelection({ type: 'part', elementId: mockPart._id, }) }) + // Open component after part is selected (as used in rundownview) + const { container } = render(, { wrapper }) - //@ts-expect-error error because avoiding an undefined type - if (!rendered) throw new Error('Component not rendered') - - // Force a rerender - await act(async () => { - rendered.rerender() - }) - - // Wait for the header element to appear - await screen.findByText('PART : Part part1') - - const header = rendered.container.querySelector('.propertiespanel-pop-up__header') - const button = rendered.container.querySelector('.propertiespanel-pop-up__button') + await waitFor( + () => { + expect(screen.getByText(`PART : ${mockPart.title}`)).toBeInTheDocument() + }, + { timeout: 1000 } + ) - expect(header).toHaveTextContent('PART : Part part1') - expect(button).toBeTruthy() + const button = container.querySelector('.propertiespanel-pop-up__button') + expect(button).toBeInTheDocument() }) test('handles user edit operations for segments', async () => { const mockSegment = createMockSegment('segment1') mockSegmentsCollection.insert(mockSegment) - // First render the selection hook const { result } = renderHook(() => useSelection(), { wrapper }) - - // Then render the properties panel const { container } = render(, { wrapper }) - // Update selection using the hook result - act(() => { + await act(async () => { result.current.clearAndSetSelection({ type: 'segment', elementId: mockSegment._id, }) }) - const switchButton = container.querySelector('.propertiespanel-pop-up__switchbutton') + // Wait for the switch button to be available + const switchButton = await waitFor(() => container.querySelector('.propertiespanel-pop-up__switchbutton')) expect(switchButton).toBeTruthy() // Toggle the switch - await userEvent.click(switchButton!) + await act(async () => { + await userEvent.click(switchButton!) + }) // Check if commit button is enabled const commitButton = screen.getByText('COMMIT CHANGES') expect(commitButton).toBeEnabled() // Commit changes - await userEvent.click(commitButton) + await act(async () => { + await userEvent.click(commitButton) + }) expect(MeteorCall.userAction.executeUserChangeOperation).toHaveBeenCalledWith( expect.anything(), @@ -305,27 +318,29 @@ describe('PropertiesPanel', () => { const mockSegment = createMockSegment('segment1') mockSegmentsCollection.insert(mockSegment) - // First render the selection hook const { result } = renderHook(() => useSelection(), { wrapper }) - - // Then render the properties panel const { container } = render(, { wrapper }) - // Update selection using the hook result - act(() => { + await act(async () => { result.current.clearAndSetSelection({ type: 'segment', elementId: mockSegment._id, }) }) + // Wait for the switch button to be available + const switchButton = await waitFor(() => container.querySelector('.propertiespanel-pop-up__switchbutton')) + // Make a change - const switchButton = container.querySelector('.propertiespanel-pop-up__switchbutton') - await userEvent.click(switchButton!) + await act(async () => { + await userEvent.click(switchButton!) + }) // Click revert button const revertButton = screen.getByText('REVERT CHANGES') - await userEvent.click(revertButton) + await act(async () => { + await userEvent.click(revertButton) + }) expect(MeteorCall.userAction.executeUserChangeOperation).toHaveBeenCalledWith( expect.anything(), @@ -340,30 +355,28 @@ describe('PropertiesPanel', () => { id: 'REVERT_SEGMENT', } ) - }) + }, 10000) // Increase timeout for this test test('closes panel when close button is clicked', async () => { const mockSegment = createMockSegment('segment1') mockSegmentsCollection.insert(mockSegment) - // First render the selection hook const { result } = renderHook(() => useSelection(), { wrapper }) - - // Then render the properties panel const { container } = render(, { wrapper }) - // Update selection using the hook result - act(() => { + await act(async () => { result.current.clearAndSetSelection({ type: 'segment', elementId: mockSegment._id, }) }) - const closeButton = container.querySelector('.propertiespanel-pop-up_close') + const closeButton = await waitFor(() => container.querySelector('.propertiespanel-pop-up_close')) expect(closeButton).toBeTruthy() - await userEvent.click(closeButton!) + await act(async () => { + await userEvent.click(closeButton!) + }) expect(container.querySelector('.propertiespanel-pop-up__contents')).toBeFalsy() }) From b86bc2176666013e66e95e10aeefb0dbbcf6ebe4 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 11 Nov 2024 11:58:27 +0100 Subject: [PATCH 71/90] wip: properties panel - element selection styling --- .../src/client/styles/elementSelected.scss | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/webui/src/client/styles/elementSelected.scss b/packages/webui/src/client/styles/elementSelected.scss index 1c1b0060a2..8557406f80 100644 --- a/packages/webui/src/client/styles/elementSelected.scss +++ b/packages/webui/src/client/styles/elementSelected.scss @@ -1,11 +1,18 @@ @import '_variables'; .element-selected { - animation: blinker 1.5s linear infinite; + // Base glow + box-shadow: inset 0 0 15px var(--primary-color, #ffffff7a); + // Changed animation name from 'blinker' to 'glow' to better reflect its purpose + animation: glow 1s ease-in-out infinite; - @keyframes blinker { - 20% { - opacity: 0.2; - } - } -} + @keyframes glow { + 0%, 100% { + box-shadow: inset 0 0 15px var(--primary-color, #ffffff7b); + } + 50% { + box-shadow: inset 0 0 20px var(--primary-color, #ffffff84), + inset 0 0 30px var(--primary-light-color, #ffffff86); + } + } +} \ No newline at end of file From e6d4c94945250e8ee6a3e1b67ab6875670947015 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 11 Nov 2024 12:19:20 +0100 Subject: [PATCH 72/90] wip: properties panel styling edit pencil icon --- packages/webui/src/client/styles/propertiesPanel.scss | 2 +- .../src/client/ui/UserEditOperations/PropertiesPanel.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index e4f493bccd..48a5185b61 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -197,7 +197,7 @@ } > .properties-panel-pop-up__has-been-edited { - background-color: #ba0b9d24; + background-color: #ffffff16; border-radius: 8px; padding: 4px 4px; } diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 7043c4b7d5..ce9d070705 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -29,6 +29,7 @@ import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SchemaFormInPlace } from '../../lib/forms/SchemaFormInPlace' import { RundownUtils } from '../../lib/rundown' import * as CoreIcon from '@nrk/core-icons/jsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' interface PendingChange { operationId: string @@ -376,7 +377,12 @@ function EditingTypeAction(props: {
- {' '} + {hasBeenEdited && ( + <> + {' '} + + + )}{' '} {translateMessage(props.userEditOperation.label, i18nTranslator)}
@@ -534,6 +540,7 @@ function EditingTypeChangeSourceLayerSource(props: { )} onChange={handleSourceChange} > + {hasBeenEdited && } Date: Tue, 12 Nov 2024 07:57:35 +0100 Subject: [PATCH 73/90] wip: properties panel close when notification is open --- .../RundownView/RundownRightHandControls.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx index 9bec7803a2..9ccb688e65 100644 --- a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx @@ -26,7 +26,6 @@ import { MediaStatusPopUp } from './MediaStatusPopUp' import { MediaStatusIcon } from '../../lib/ui/icons/mediaStatus' import { SelectedElementsContext } from './SelectedElementsContext' import { UserEditsCloseIcon, UserEditsIcon } from '../../lib/ui/icons/useredits' -import { CollapseChevrons } from '../../lib/ui/icons/notifications' interface IProps { playlistId: RundownPlaylistId @@ -157,16 +156,19 @@ export function RundownRightHandControls(props: Readonly): JSX.Element { title={t('Notes')} /> - {(context) => ( - - )} + {(context) => { + const isOpen = context.listSelectedElements().length > 0 && !props.isNotificationCenterOpen + return ( + + ) + }}
From 958d8e3128386737d05725b846404197dda78844 Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 13 Nov 2024 10:11:29 +0100 Subject: [PATCH 76/90] fix: Properties panel - StyledSchemaFormInPlace schema for normal form --- .../src/client/ui/UserEditOperations/PropertiesPanel.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 56ecfc9081..cb8c3d408e 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -414,10 +414,11 @@ function EditingTypeChangeForm(props: { {schema && ( <> {t('Source')}: -

@@ -543,8 +544,6 @@ function EditingTypeChangeSourceLayerSource(props: { schema={selectedGroupSchema} object={selectedValues} translationNamespaces={props.userEditOperation.translationNamespaces} - compact={false} - className="custom-class" width="100%" />
From 7f5fa091f0841ea7220fd69856660cd79a9dd8f0 Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 13 Nov 2024 10:33:27 +0100 Subject: [PATCH 77/90] feat: Properties panel is edited pencil styling --- .../src/client/styles/propertiesPanel.scss | 20 ++++++++++++----- .../ui/UserEditOperations/PropertiesPanel.tsx | 22 +++++++------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 48a5185b61..296a6e37f7 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -188,14 +188,24 @@ margin-top: 15px; color: #ddd; padding: 4px 4px; - + position: relative; + display: flex; + align-items: flex-start; + gap: 8px; + + // Add positioning for the pencil icon + > svg { + margin-top: 4px; + flex-shrink: 0; + } + > .properties-panel-pop-up__form__schema { - border-color: pink; - border-width: 0px; - + border-color: pink; + border-width: 0px; + flex-grow: 1; } } - + > .properties-panel-pop-up__has-been-edited { background-color: #ffffff16; border-radius: 8px; diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index cb8c3d408e..db22b0311a 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -360,6 +360,12 @@ function EditingTypeAction(props: { hasBeenEdited ? 'properties-panel-pop-up__has-been-edited' : '' )} > + {hasBeenEdited && ( + <> + {' '} + + + )}{' '} - {hasBeenEdited && ( - <> - {' '} - - - )}{' '} {translateMessage(props.userEditOperation.label, i18nTranslator)}
@@ -532,14 +532,8 @@ function EditingTypeChangeSourceLayerSource(props: { hasBeenEdited ? 'properties-panel-pop-up__has-been-edited' : '' )} > -
- {hasBeenEdited && } + {hasBeenEdited && } +
Date: Wed, 13 Nov 2024 10:43:54 +0100 Subject: [PATCH 78/90] feat: Properties header styling --- packages/webui/src/client/styles/propertiesPanel.scss | 1 - .../src/client/ui/UserEditOperations/PropertiesPanel.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 296a6e37f7..3c096419cb 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -65,7 +65,6 @@ font-size: 1.2em; text-align: left; - padding-top: 0.5rem; padding-left: 1rem; display: flex; align-items: center; diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index db22b0311a..4311802320 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -148,7 +148,7 @@ export function PropertiesPanel(): JSX.Element { >
) })} - PART : {String(part?.title)} + {part?.title.slice(0, 30)}
{segment && @@ -218,7 +218,7 @@ export function PropertiesPanel(): JSX.Element { >
) })} - SEGMENT : {String(segment?.name)} + {segment?.name.slice(0, 30)}
{segment && From 75b1930d2b0e9f07668becdeda90d9cb2ff10f7d Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 13 Nov 2024 11:41:58 +0100 Subject: [PATCH 79/90] wip: Properties tests - more mocking to get correct rendering in test to work --- .../__tests__/PropertiesPanel.test.tsx | 104 ++++++++++++++---- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx index 7ca0d5e5f1..fd789195d5 100644 --- a/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx @@ -1,49 +1,96 @@ -jest.mock( - '../../../../__mocks__/tracker', - () => ({ +jest.mock('../../../../__mocks__/tracker', () => { + interface TrackerComputation { + stop: () => void + _recompute: () => void + invalidate: () => void + onInvalidate: () => void + } + const computations = new Set() + + return { setup: () => ({ Tracker: { autorun: jest.fn((fn) => { - fn() - return { + const computation = { stop: jest.fn(), + _recompute: () => fn(computation), + invalidate: function () { + this._recompute() + }, + onInvalidate: jest.fn(), } + computations.add(computation) + fn(computation) + return computation }), nonreactive: jest.fn((fn) => fn()), active: false, currentComputation: null, - Dependency: jest.fn().mockImplementation(() => ({ - depend: jest.fn(), - changed: jest.fn(), - hasDependents: jest.fn(), - })), + afterFlush: (fn: () => void) => { + setTimeout(fn, 0) + }, + flush: () => { + computations.forEach((comp) => comp._recompute()) + }, + Dependency: jest.fn().mockImplementation(() => { + const dependents = new Set() + return { + depend: jest.fn(() => { + if (Tracker.currentComputation) { + dependents.add(Tracker.currentComputation as any as TrackerComputation) + } + }), + changed: jest.fn(() => { + dependents.forEach((comp) => comp.invalidate()) + }), + hasDependents: jest.fn(() => dependents.size > 0), + } + }), }, }), - }), - { - virtual: true, } -) +}) // Mock the ReactiveDataHelper: jest.mock('../../../lib/reactiveData/ReactiveDataHelper', () => { + interface MockSubscription { + stop: () => void + ready: () => boolean + } + class MockReactiveDataHelper { - protected _subs: Array<{ stop: () => void }> = [] + protected _subs: MockSubscription[] = [] + protected _computations: any[] = [] - protected subscribe() { - const sub = { stop: jest.fn() } + protected subscribe(_name: string, ..._args: any[]): MockSubscription { + const sub: MockSubscription = { + stop: jest.fn(), + ready: jest.fn().mockReturnValue(true), + } this._subs.push(sub) return sub } protected autorun(f: () => void) { + // Execute the function immediately f() - return { stop: jest.fn() } + const computation = { + stop: jest.fn(), + _recompute: () => f(), + invalidate: function () { + this._recompute() + }, + onInvalidate: jest.fn(), + } + this._computations.push(computation) + return computation } destroy() { this._subs.forEach((sub) => sub.stop()) + this._computations.forEach((comp) => comp.stop()) this._subs = [] + this._computations = [] } } @@ -51,6 +98,10 @@ jest.mock('../../../lib/reactiveData/ReactiveDataHelper', () => { constructor() { super() } + + triggerUpdate() { + this._computations.forEach((comp) => comp.invalidate()) + } } return { @@ -58,6 +109,7 @@ jest.mock('../../../lib/reactiveData/ReactiveDataHelper', () => { WithManagedTracker: MockWithManagedTracker, meteorSubscribe: jest.fn().mockReturnValue({ stop: jest.fn(), + ready: jest.fn().mockReturnValue(true), }), } }) @@ -114,6 +166,8 @@ import { SelectedElementProvider, useSelection } from '../../RundownView/Selecte import { MongoMock } from '../../../../__mocks__/mongo' import { PropertiesPanel } from '../PropertiesPanel' import { UserAction } from '../../../lib/clientUserAction' +import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Tracker } from 'meteor/tracker' const mockSegmentsCollection = MongoMock.getInnerMockCollection(Segments) const mockPartsCollection = MongoMock.getInnerMockCollection(UIParts) @@ -203,9 +257,13 @@ describe('PropertiesPanel', () => { test('renders segment properties when segment is selected', async () => { const mockSegment = createMockSegment('segment1') - mockSegmentsCollection.insert(mockSegment) + const mockId = mockSegmentsCollection.insert(mockSegment) as any as SegmentId + + const verifySegment = mockSegmentsCollection.findOne({ _id: mockId }) + expect(verifySegment).toBeTruthy() + console.log('Verify segment :', verifySegment?._id) - expect(mockSegmentsCollection.findOne({ _id: mockSegment._id })).toBeTruthy() + expect(mockSegmentsCollection.findOne({ _id: mockId })).toBeTruthy() const { result } = renderHook(() => useSelection(), { wrapper }) @@ -213,10 +271,14 @@ describe('PropertiesPanel', () => { await act(async () => { result.current.clearAndSetSelection({ type: 'segment', - elementId: mockSegment._id, + elementId: mockId, }) }) + await act(async () => { + jest.advanceTimersByTime(100) + }) + // Open component after segment is selected (as used in rundownview) const { container } = render(, { wrapper }) From 91dae54f0c9c1d10da67b0b1b36d48e3b7e4b39d Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Thu, 21 Nov 2024 09:41:33 +0000 Subject: [PATCH 80/90] chore: fix unit tests --- .../ui/UserEditOperations/PropertiesPanel.tsx | 14 +- .../__tests__/PropertiesPanel.test.tsx | 183 ++++++++---------- 2 files changed, 82 insertions(+), 115 deletions(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 4311802320..35197eccb7 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { i18nTranslator } from '../i18n' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { doUserAction, UserAction } from '../../lib/clientUserAction' import { MeteorCall } from '../../lib/meteorApi' @@ -30,6 +29,7 @@ import { StyledSchemaFormInPlace } from '../../lib/forms/SchemaFormInPlace' import { RundownUtils } from '../../lib/rundown' import * as CoreIcon from '@nrk/core-icons/jsx' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faPencilAlt } from '@fortawesome/free-solid-svg-icons' interface PendingChange { operationId: string @@ -297,6 +297,8 @@ function EditingTypeAction(props: { pendingChanges: PendingChange[] setPendingChanges: React.Dispatch> }) { + const { t } = useTranslation() + const getPendingState = () => { const pendingChange = props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id) return pendingChange?.switchState @@ -347,9 +349,7 @@ function EditingTypeAction(props: { className="propertiespanel-pop-up__button" onClick={addPendingChange} > - - {translateMessage(props.userEditOperation.label, i18nTranslator)} - + {translateMessage(props.userEditOperation.label, t)} ) case UserEditingButtonType.SWITCH: @@ -363,7 +363,7 @@ function EditingTypeAction(props: { {hasBeenEdited && ( <> {' '} - + )}{' '}
- - {translateMessage(props.userEditOperation.label, i18nTranslator)} - + {translateMessage(props.userEditOperation.label, t)}
) case UserEditingButtonType.HIDDEN || undefined: diff --git a/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx index fd789195d5..188efabd72 100644 --- a/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/__tests__/PropertiesPanel.test.tsx @@ -1,58 +1,48 @@ -jest.mock('../../../../__mocks__/tracker', () => { - interface TrackerComputation { - stop: () => void - _recompute: () => void - invalidate: () => void - onInvalidate: () => void - } - const computations = new Set() +import React from 'react' +// eslint-disable-next-line node/no-unpublished-import +import { renderHook, act, render, screen, waitFor, RenderOptions } from '@testing-library/react' +// eslint-disable-next-line node/no-unpublished-import +import '@testing-library/jest-dom' +import { MeteorCall } from '../../../lib/meteorApi' +import { TFunction } from 'i18next' - return { - setup: () => ({ - Tracker: { - autorun: jest.fn((fn) => { - const computation = { - stop: jest.fn(), - _recompute: () => fn(computation), - invalidate: function () { - this._recompute() - }, - onInvalidate: jest.fn(), - } - computations.add(computation) - fn(computation) - return computation - }), - nonreactive: jest.fn((fn) => fn()), - active: false, - currentComputation: null, - afterFlush: (fn: () => void) => { - setTimeout(fn, 0) - }, - flush: () => { - computations.forEach((comp) => comp._recompute()) - }, - Dependency: jest.fn().mockImplementation(() => { - const dependents = new Set() - return { - depend: jest.fn(() => { - if (Tracker.currentComputation) { - dependents.add(Tracker.currentComputation as any as TrackerComputation) - } - }), - changed: jest.fn(() => { - dependents.forEach((comp) => comp.invalidate()) - }), - hasDependents: jest.fn(() => dependents.size > 0), - } - }), +import userEvent from '@testing-library/user-event' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { UIParts } from '../../Collections' +import { Segments } from '../../../../client/collections' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { UserEditingType, UserEditingButtonType } from '@sofie-automation/blueprints-integration' +import { + SelectedElementProvider, + SelectedElementsContext, + SelectionContextType, + useSelection, +} from '../../RundownView/SelectedElementsContext' +import { MongoMock } from '../../../../__mocks__/mongo' +import { PropertiesPanel } from '../PropertiesPanel' +import { UserAction } from '../../../lib/clientUserAction' + +jest.mock('meteor/tracker', (...args) => require('../../../../__mocks__/tracker').setup(args), { virtual: true }) + +jest.mock('react-i18next', () => ({ + // this mock makes sure any components using the translate hook can use it without a warning being shown + useTranslation: () => { + return { + t: (str: string) => str, + i18n: { + changeLanguage: () => new Promise(() => {}), }, - }), - } -}) + } + }, + initReactI18next: { + type: '3rdParty', + init: () => {}, + }, +})) // Mock the ReactiveDataHelper: -jest.mock('../../../lib/reactiveData/ReactiveDataHelper', () => { +jest.mock('../../../lib/reactiveData/reactiveDataHelper', () => { interface MockSubscription { stop: () => void ready: () => boolean @@ -147,28 +137,6 @@ jest.mock('react-i18next', () => ({ }, })) -import React from 'react' -// eslint-disable-next-line node/no-unpublished-import -import { renderHook, act, render, screen, waitFor } from '@testing-library/react' -// eslint-disable-next-line node/no-unpublished-import -import '@testing-library/jest-dom' -import { MeteorCall } from '../../../lib/meteorApi' -import { TFunction } from 'i18next' - -import userEvent from '@testing-library/user-event' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { UIParts } from '../../Collections' -import { Segments } from '../../../../client/collections' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { UserEditingType, UserEditingButtonType } from '@sofie-automation/blueprints-integration' -import { SelectedElementProvider, useSelection } from '../../RundownView/SelectedElementsContext' -import { MongoMock } from '../../../../__mocks__/mongo' -import { PropertiesPanel } from '../PropertiesPanel' -import { UserAction } from '../../../lib/clientUserAction' -import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Tracker } from 'meteor/tracker' - const mockSegmentsCollection = MongoMock.getInnerMockCollection(Segments) const mockPartsCollection = MongoMock.getInnerMockCollection(UIParts) @@ -177,6 +145,9 @@ jest.mock('../../../lib/clientUserAction', () => ({ doUserAction: jest.fn((_t: TFunction, e: unknown, _action: UserAction, callback: Function) => callback(e, Date.now()) ), + UserAction: { + EXECUTE_USER_OPERATION: 51, + }, })) // Mock Userchange Operation: @@ -199,11 +170,21 @@ describe('PropertiesPanel', () => { {children} ) + const renderWithContext = ( + ui: React.ReactNode, + { ctxValue, ...renderOptions }: RenderOptions & { ctxValue: SelectionContextType } + ) => { + return render( + {ui}, + renderOptions + ) + } + beforeEach(() => { mockSegmentsCollection.remove({}) mockPartsCollection.remove({}) jest.clearAllMocks() - jest.useFakeTimers() + // jest.useFakeTimers() }) afterEach(() => { @@ -257,13 +238,12 @@ describe('PropertiesPanel', () => { test('renders segment properties when segment is selected', async () => { const mockSegment = createMockSegment('segment1') - const mockId = mockSegmentsCollection.insert(mockSegment) as any as SegmentId + const mockId = mockSegmentsCollection.insert(mockSegment) + const protectedMockId = protectString(mockId) - const verifySegment = mockSegmentsCollection.findOne({ _id: mockId }) + const verifySegment = mockSegmentsCollection.findOne({ _id: protectedMockId }) expect(verifySegment).toBeTruthy() - console.log('Verify segment :', verifySegment?._id) - - expect(mockSegmentsCollection.findOne({ _id: mockId })).toBeTruthy() + expect(mockSegmentsCollection.findOne({ _id: protectedMockId })).toBeTruthy() const { result } = renderHook(() => useSelection(), { wrapper }) @@ -271,29 +251,17 @@ describe('PropertiesPanel', () => { await act(async () => { result.current.clearAndSetSelection({ type: 'segment', - elementId: mockId, + elementId: protectedMockId, }) }) - await act(async () => { - jest.advanceTimersByTime(100) - }) + expect(result.current.listSelectedElements()).toHaveLength(1) + expect(result.current.listSelectedElements()).toEqual([{ type: 'segment', elementId: mockId }]) // Open component after segment is selected (as used in rundownview) - const { container } = render(, { wrapper }) - - await act(async () => { - jest.advanceTimersByTime(100) - }) + const { container } = renderWithContext(, { ctxValue: result.current }) - console.log('result', result.current.listSelectedElements()) - // Use findByTestId instead of querySelector - await waitFor( - () => { - expect(screen.getByText(`SEGMENT : ${mockSegment.name}`)).toBeInTheDocument() - }, - { timeout: 1000 } - ) + expect(screen.getByText(`${mockSegment.name.slice(0, 30)}`)).toBeInTheDocument() const button = container.querySelector('.propertiespanel-pop-up__button') expect(button).toBeInTheDocument() @@ -304,22 +272,22 @@ describe('PropertiesPanel', () => { const mockPart = createMockPart('part1', String(mockSegment._id)) mockSegmentsCollection.insert(mockSegment) - mockPartsCollection.insert(mockPart) + const mockId = mockPartsCollection.insert(mockPart) const { result } = renderHook(() => useSelection(), { wrapper }) await act(async () => { result.current.clearAndSetSelection({ type: 'part', - elementId: mockPart._id, + elementId: protectString(mockId), }) }) // Open component after part is selected (as used in rundownview) - const { container } = render(, { wrapper }) + const { container } = renderWithContext(, { ctxValue: result.current }) await waitFor( () => { - expect(screen.getByText(`PART : ${mockPart.title}`)).toBeInTheDocument() + expect(screen.getByText(mockPart.title.slice(0, 30))).toBeInTheDocument() }, { timeout: 1000 } ) @@ -333,7 +301,6 @@ describe('PropertiesPanel', () => { mockSegmentsCollection.insert(mockSegment) const { result } = renderHook(() => useSelection(), { wrapper }) - const { container } = render(, { wrapper }) await act(async () => { result.current.clearAndSetSelection({ @@ -343,13 +310,14 @@ describe('PropertiesPanel', () => { }) // Wait for the switch button to be available + const { container } = renderWithContext(, { ctxValue: result.current }) const switchButton = await waitFor(() => container.querySelector('.propertiespanel-pop-up__switchbutton')) expect(switchButton).toBeTruthy() + if (!switchButton) return // above would have thrown - this is a type guard + // Toggle the switch - await act(async () => { - await userEvent.click(switchButton!) - }) + await userEvent.click(switchButton) // Check if commit button is enabled const commitButton = screen.getByText('COMMIT CHANGES') @@ -381,7 +349,6 @@ describe('PropertiesPanel', () => { mockSegmentsCollection.insert(mockSegment) const { result } = renderHook(() => useSelection(), { wrapper }) - const { container } = render(, { wrapper }) await act(async () => { result.current.clearAndSetSelection({ @@ -390,6 +357,8 @@ describe('PropertiesPanel', () => { }) }) + const { container } = renderWithContext(, { ctxValue: result.current }) + // Wait for the switch button to be available const switchButton = await waitFor(() => container.querySelector('.propertiespanel-pop-up__switchbutton')) @@ -414,10 +383,10 @@ describe('PropertiesPanel', () => { pieceExternalId: undefined, }, { - id: 'REVERT_SEGMENT', + id: 'revert-segment', } ) - }, 10000) // Increase timeout for this test + }) test('closes panel when close button is clicked', async () => { const mockSegment = createMockSegment('segment1') From 77bf2271e0fe753c707a7e7e0a9e18081613d4d8 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 27 Nov 2024 15:13:25 +0000 Subject: [PATCH 81/90] feat: refactor form action to properties field --- .../src/documents/part.ts | 8 +- .../src/documents/segment.ts | 8 +- packages/blueprints-integration/src/ingest.ts | 20 +- .../blueprints-integration/src/userEditing.ts | 44 +- packages/corelib/src/dataModel/Part.ts | 8 +- packages/corelib/src/dataModel/Segment.ts | 8 +- .../src/dataModel/UserEditingDefinitions.ts | 42 ++ .../job-worker/src/blueprints/context/lib.ts | 4 + .../client/lib/forms/SchemaFormWithState.tsx | 72 +++ .../ui/UserEditOperations/PropertiesPanel.tsx | 578 ++++++------------ 10 files changed, 377 insertions(+), 415 deletions(-) create mode 100644 packages/webui/src/client/lib/forms/SchemaFormWithState.tsx diff --git a/packages/blueprints-integration/src/documents/part.ts b/packages/blueprints-integration/src/documents/part.ts index ea4f05cfdc..0807a2183b 100644 --- a/packages/blueprints-integration/src/documents/part.ts +++ b/packages/blueprints-integration/src/documents/part.ts @@ -1,4 +1,4 @@ -import { UserEditingDefinition } from '../userEditing' +import { UserEditingDefinition, UserEditingProperties } from '../userEditing' import type { NoteSeverity } from '../lib' import type { ITranslatableMessage } from '../translations' @@ -88,6 +88,12 @@ export interface IBlueprintMutatablePart } export interface HackPartMediaObjectSubscription { diff --git a/packages/blueprints-integration/src/documents/segment.ts b/packages/blueprints-integration/src/documents/segment.ts index 6a8fcadb6e..36502a893a 100644 --- a/packages/blueprints-integration/src/documents/segment.ts +++ b/packages/blueprints-integration/src/documents/segment.ts @@ -1,4 +1,4 @@ -import { UserEditingDefinition } from '../userEditing' +import { UserEditingDefinition, UserEditingProperties } from '../userEditing' export enum SegmentDisplayMode { Timeline = 'timeline', @@ -52,6 +52,12 @@ export interface IBlueprintSegment diff --git a/packages/blueprints-integration/src/ingest.ts b/packages/blueprints-integration/src/ingest.ts index e4594620e6..4c8c3dc20d 100644 --- a/packages/blueprints-integration/src/ingest.ts +++ b/packages/blueprints-integration/src/ingest.ts @@ -125,9 +125,10 @@ export interface UserOperationTarget { } export enum DefaultUserOperationsTypes { - REVERT_SEGMENT = 'revert-segment', - REVERT_PART = 'revert-part', - REVERT_RUNDOWN = 'revert-rundown', + REVERT_SEGMENT = '__sofie-revert-segment', + REVERT_PART = '__sofie-revert-part', + REVERT_RUNDOWN = '__sofie-revert-rundown', + UPDATE_PROPS = '__sofie-update-props', } export interface DefaultUserOperationRevertRundown { @@ -144,14 +145,19 @@ export interface DefaultUserOperationRevertPart { id: DefaultUserOperationsTypes.REVERT_PART } +export interface DefaultUserOperationEditProperties { + id: DefaultUserOperationsTypes.UPDATE_PROPS + payload: { + pieceTypeProperties: { type: string; value: Record } + globalProperties: Record + } +} + export type DefaultUserOperations = - | { - id: '__sofie-move-segment' // Future: define properly - payload: Record - } | DefaultUserOperationRevertRundown | DefaultUserOperationRevertSegment | DefaultUserOperationRevertPart + | DefaultUserOperationEditProperties export interface UserOperationChange { /** Indicate that this change is from user operations */ diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index a42e821c70..8807c8fe3e 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -74,7 +74,7 @@ export enum UserEditingType { } export interface UserEditingSourceLayer { - sourceLayerLabel: string + sourceLayerLabel: string // translate? take from type? sourceLayerType: SourceLayerType schema: JSONBlob } @@ -87,3 +87,45 @@ export enum UserEditingButtonType { /** Hidden */ HIDDEN = 'hidden', } + +export interface UserEditingProperties { + /** + * These properties are dependent on the (primary) piece type, the user will get the option + * to select the type of piece (from the SourceLayerTypes i.e. Camera or Split etc.) and then + * be presented the corresponding form + * + * example: + * { + * schema: { + * camera: '{ "type": "object", "properties": { "input": { "type": "number" } } }', + * split: '{ "type": "object", ... }', + * }, + * currentValue: { + * type: 'camera', + * value: { + * input: 3 + * }, + * } + * } + */ + pieceTypeProperties?: { + schema: Record + currentValue: { type: string; value: Record } + } + + /** + * These are properties that are available to edit regardless of the piece type, examples + * could be whether it an element is locked from NRCS updates + * + * if you do not want the piece type to be changed, then use only this field. + */ + globalProperties?: { schema: JSONBlob; currentValue: Record } + + /** + * A list of id's of operations to be exposed on the properties panel as buttons. These operations + * must be available on the element + * + * note - perhaps these should have their own full definitions? + */ + operations?: string[] +} diff --git a/packages/corelib/src/dataModel/Part.ts b/packages/corelib/src/dataModel/Part.ts index 5194cb98b8..a5bf7a1c31 100644 --- a/packages/corelib/src/dataModel/Part.ts +++ b/packages/corelib/src/dataModel/Part.ts @@ -3,7 +3,7 @@ import { ITranslatableMessage } from '../TranslatableMessage' import { PartId, RundownId, SegmentId } from './Ids' import { PartNote } from './Notes' import { ReadonlyDeep } from 'type-fest' -import { CoreUserEditingDefinition } from './UserEditingDefinitions' +import { CoreUserEditingDefinition, UserEditingProperties } from './UserEditingDefinitions' export interface PartInvalidReason { message: ITranslatableMessage @@ -41,6 +41,12 @@ export interface DBPart extends Omit { * User editing definitions for this part */ userEditOperations?: CoreUserEditingDefinition[] + + /** + * 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 function isPartPlayable(part: Pick, 'invalid' | 'floated'>): boolean { diff --git a/packages/corelib/src/dataModel/Segment.ts b/packages/corelib/src/dataModel/Segment.ts index 89b03d102f..580e2daefd 100644 --- a/packages/corelib/src/dataModel/Segment.ts +++ b/packages/corelib/src/dataModel/Segment.ts @@ -1,7 +1,7 @@ import { SegmentDisplayMode, SegmentTimingInfo } from '@sofie-automation/blueprints-integration' import { SegmentId, RundownId } from './Ids' import { SegmentNote } from './Notes' -import { CoreUserEditingDefinition } from './UserEditingDefinitions' +import { CoreUserEditingDefinition, UserEditingProperties } from './UserEditingDefinitions' export enum SegmentOrphanedReason { /** Segment is deleted from the NRCS but we still need it */ @@ -51,4 +51,10 @@ export interface DBSegment { * User editing definitions for this segment */ userEditOperations?: CoreUserEditingDefinition[] + + /** + * 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 } diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index b7db352204..31bcf1bf08 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -66,3 +66,45 @@ export interface CoreUserEditingDefinitionSourceLayerForm { value: Record } } + +export interface UserEditingProperties { + /** + * These properties are dependent on the (primary) piece type, the user will get the option + * to select the type of piece (from the SourceLayerTypes i.e. Camera or Split etc.) and then + * be presented the corresponding form + * + * example: + * { + * schema: { + * camera: '{ "type": "object", "properties": { "input": { "type": "number" } } }', + * split: '{ "type": "object", ... }', + * }, + * currentValue: { + * type: 'camera', + * value: { + * input: 3 + * }, + * } + * } + */ + pieceTypeProperties?: { + schema: Record + currentValue: { type: string; value: Record } + } + + /** + * These are properties that are available to edit regardless of the piece type, examples + * could be whether it an element is locked from NRCS updates + * + * if you do not want the piece type to be changed, then use only this field. + */ + globalProperties?: { schema: JSONBlob; currentValue: Record } + + /** + * A list of id's of operations to be exposed on the properties panel as buttons. These operations + * must be available on the element + * + * note - perhaps these should have their own full definitions? + */ + operations?: string[] +} diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 3335b57e24..a9c84085e6 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -14,6 +14,7 @@ import { CoreUserEditingDefinitionAction, CoreUserEditingDefinitionForm, CoreUserEditingDefinitionSourceLayerForm, + UserEditingProperties, } from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { assertNever, clone, Complete, literal, omit } from '@sofie-automation/corelib/dist/lib' @@ -121,6 +122,7 @@ export const IBlueprintMutatablePartSampleKeys = allKeysOfObject): IBlueprintP part.hackListenToMediaObjectUpdates ), userEditOperations: translateUserEditsToBlueprint(part.userEditOperations), + userEditProperties: clone(part.userEditProperties), } return obj @@ -349,6 +352,7 @@ export function convertSegmentToBlueprints(segment: ReadonlyDeep): IB showShelf: segment.showShelf, segmentTiming: segment.segmentTiming, userEditOperations: translateUserEditsToBlueprint(segment.userEditOperations), + userEditProperties: clone(segment.userEditProperties), } return obj diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx new file mode 100644 index 0000000000..e34530a3d3 --- /dev/null +++ b/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx @@ -0,0 +1,72 @@ +import { useCallback, useMemo, useState } from 'react' +import { + OverrideOpHelperForItemContentsBatcher, + WrappedOverridableItemNormal, +} from '../../ui/Settings/util/OverrideOpHelper' +import { SchemaFormCommonProps } from './schemaFormUtil' +import { SchemaFormWithOverrides } from './SchemaFormWithOverrides' +import { literal, objectPathSet } from '@sofie-automation/corelib/dist/lib' +import { AnyARecord } from 'dns' + +interface SchemaFormWithStateProps extends Omit { + object: any + + onUpdate: (object: any) => void +} + +export function SchemaFormWithState({ + object, + onUpdate, + ...commonProps +}: Readonly): JSX.Element { + const helper = useCallback( + () => + new OverrideOpHelperWithState(object, (object) => { + onUpdate(object) + }), + [object, onUpdate] + ) + + const wrappedItem = useMemo( + () => + literal>({ + type: 'normal', + id: 'not-used', + computed: object, + defaults: undefined, + overrideOps: [], + }), + [object] + ) + + return +} + +/** + * An alternate OverrideOpHelper designed to directly mutate an object, instead of using the `ObjectWithOverrides` system. + * This allows us to have one SchemaForm implementation that can handle working with `ObjectWithOverrides`, and simpler options + */ +class OverrideOpHelperWithState implements OverrideOpHelperForItemContentsBatcher { + readonly #object: any + readonly #onUpdate: (object: any) => void + + constructor(object: AnyARecord, onUpdate: (object: any) => void) { + this.#object = object + this.#onUpdate = onUpdate + } + + clearItemOverrides(_itemId: string, _subPath: string): this { + // Not supported as this is faking an item with overrides + + return this + } + setItemValue(_itemId: string, subPath: string, value: any): this { + objectPathSet(this.#object, subPath, value) + + return this + } + + commit(): void { + this.#onUpdate(this.#object) + } +} diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 35197eccb7..f07de4a68d 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -1,52 +1,38 @@ import * as React from 'react' -import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { doUserAction, UserAction } from '../../lib/clientUserAction' import { MeteorCall } from '../../lib/meteorApi' import { + DefaultUserOperationEditProperties, DefaultUserOperationsTypes, + JSONBlob, JSONBlobParse, - SourceLayerType, - UserEditingButtonType, + JSONSchema, + UserEditingProperties, UserEditingSourceLayer, UserEditingType, } from '@sofie-automation/blueprints-integration' -import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' +import { literal } from '@sofie-automation/corelib/dist/lib' import classNames from 'classnames' -import { - CoreUserEditingDefinitionAction, - CoreUserEditingDefinitionForm, - CoreUserEditingDefinitionSourceLayerForm, -} from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' import { useTranslation } from 'react-i18next' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { Segments } from '../../collections' import { UIParts } from '../Collections' import { useSelection } from '../RundownView/SelectedElementsContext' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { StyledSchemaFormInPlace } from '../../lib/forms/SchemaFormInPlace' import { RundownUtils } from '../../lib/rundown' import * as CoreIcon from '@nrk/core-icons/jsx' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPencilAlt } from '@fortawesome/free-solid-svg-icons' +import { useCallback } from 'react' +import { SchemaFormWithState } from '../../lib/forms/SchemaFormWithState' -interface PendingChange { - operationId: string - userEditingType: UserEditingType - sourceLayerType?: SourceLayerType - value?: Record - switchState?: boolean - cssTypeClass?: string -} +type PendingChange = DefaultUserOperationEditProperties['payload'] export function PropertiesPanel(): JSX.Element { const { listSelectedElements, clearSelections } = useSelection() const selectedElement = listSelectedElements()?.[0] const { t } = useTranslation() - const [pendingChanges, setPendingChanges] = React.useState([]) - const hasPendingChanges = pendingChanges.length > 0 + const [pendingChange, setPendingChange] = React.useState(undefined) + const hasPendingChanges = !!pendingChange const [isAnimatedIn, setIsAnimatedIn] = React.useState(false) React.useEffect(() => { @@ -67,7 +53,7 @@ export function PropertiesPanel(): JSX.Element { }, []) const part = useTracker(() => { - setPendingChanges([]) + setPendingChange(undefined) return UIParts.findOne({ _id: selectedElement?.elementId }) }, [selectedElement?.elementId]) @@ -78,9 +64,13 @@ export function PropertiesPanel(): JSX.Element { const rundownId = part ? part.rundownId : segment?.rundownId const handleCommitChanges = async (e: React.MouseEvent) => { - if (!rundownId || !selectedElement) return - for (const change of pendingChanges) { - doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => + if (!rundownId || !selectedElement || !pendingChange) return + + doUserAction( + t, + e, + UserAction.EXECUTE_USER_OPERATION, + (e, ts) => MeteorCall.userAction.executeUserChangeOperation( e, ts, @@ -90,20 +80,18 @@ export function PropertiesPanel(): JSX.Element { partExternalId: part?.externalId, pieceExternalId: undefined, }, - { - id: change.operationId, - values: change.value, - } - ) - ) - } - // Delay the Clear pending changes after executing to avoid async flickering: - setTimeout(() => setPendingChanges([]), 100) + literal({ + id: DefaultUserOperationsTypes.UPDATE_PROPS, + payload: pendingChange, + }) + ), + () => setPendingChange(undefined) + ) } const handleRevertChanges = (e: React.MouseEvent) => { if (!rundownId || !selectedElement) return - setPendingChanges([]) + setPendingChange(undefined) doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => MeteorCall.userAction.executeUserChangeOperation( e, @@ -124,6 +112,26 @@ export function PropertiesPanel(): JSX.Element { ) } + const userEditOperations = + selectedElement?.type === 'part' + ? part?.userEditOperations + : selectedElement?.type === 'segment' + ? segment?.userEditOperations + : undefined + const userEditProperties = + selectedElement?.type === 'part' + ? part?.userEditProperties + : selectedElement?.type === 'segment' + ? segment?.userEditProperties + : undefined + const change = pendingChange ?? { + pieceTypeProperties: userEditProperties?.pieceTypeProperties?.currentValue ?? { type: '', value: {} }, + globalProperties: userEditProperties?.globalProperties?.currentValue ?? {}, + } + + const title = + selectedElement?.type === 'part' ? part?.title : selectedElement?.type === 'segment' ? segment?.name : undefined + return (
@@ -131,143 +139,40 @@ export function PropertiesPanel(): JSX.Element {
- {rundownId && selectedElement?.type === 'part' && ( - <> -
- {part?.userEditOperations && - part.userEditOperations.map((operation) => { - if (operation.type !== UserEditingType.ACTION || !operation.svgIcon || !operation.isActive) - return null - return ( -
- ) - })} - {part?.title.slice(0, 30)} -
-
- {segment && - part?._id && - part.userEditOperations?.map((userEditOperation, i) => { - switch (userEditOperation.type) { - case UserEditingType.ACTION: - return ( - - ) - case UserEditingType.FORM: - return ( - - ) - case UserEditingType.SOURCE_LAYER_FORM: - return ( - - ) - default: - assertNever(userEditOperation) - return null - } - })} -
-
- - )} - {rundownId && selectedElement?.type === 'segment' && ( - <> -
- {segment?.userEditOperations && - segment.userEditOperations.map((operation) => { - if (operation.type !== UserEditingType.ACTION || !operation.svgIcon || !operation.isActive) - return null - - return ( -
- ) - })} - {segment?.name.slice(0, 30)} -
-
- {segment && - segment?.userEditOperations?.map((userEditOperation, i) => { - switch (userEditOperation.type) { - case UserEditingType.ACTION: - return ( - - ) - case UserEditingType.FORM: - return ( - - ) - case UserEditingType.SOURCE_LAYER_FORM: - return ( - - ) - default: - assertNever(userEditOperation) - return null - } - })} -
- - )} + <> +
+ {userEditOperations && + userEditOperations.map((operation) => { + if (operation.type !== UserEditingType.ACTION || !operation.svgIcon || !operation.isActive) return null + return ( +
+ ) + })} + {title} +
+
+ {userEditProperties?.pieceTypeProperties && ( + + )} + {userEditProperties?.globalProperties && ( + + )} +
+
- ) - case UserEditingButtonType.SWITCH: - return ( -
- {hasBeenEdited && ( - <> - {' '} - - - )}{' '} - -
-
-   -   -
-
-
-
- {translateMessage(props.userEditOperation.label, t)} -
- ) - case UserEditingButtonType.HIDDEN || undefined: - return null - default: - assertNever(props.userEditOperation.buttonType) - return null - } -} - -function EditingTypeChangeForm(props: { - userEditOperation: CoreUserEditingDefinitionForm - segment: DBSegment | undefined - part: DBPart | undefined - rundownId: RundownId - pendingChanges: PendingChange[] - setPendingChanges: React.Dispatch> -}) { - const { t } = useTranslation() - - const jsonSchema = props.userEditOperation.schema - const schema = jsonSchema ? JSONBlobParse(jsonSchema) : undefined - const values = clone(props.userEditOperation.currentValues) - - return ( - <> - {schema && ( - <> - {t('Source')}: - -
-
- - )} - - ) -} - -function EditingTypeChangeSourceLayerSource(props: { - userEditOperation: CoreUserEditingDefinitionSourceLayerForm - segment: DBSegment | undefined - part: DBPart | undefined - rundownId: RundownId - pendingChanges: PendingChange[] - setPendingChanges: React.Dispatch> -}) { - const [selectedSourceGroup, setSelectedSourceButton] = React.useState( - props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.sourceLayerType || - props.userEditOperation.currentValues.type - ) - const [selectedValues, setSelectedValues] = React.useState>( - props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id)?.value || - props.userEditOperation.currentValues.value - ) - - const getPendingState = () => { - const pendingChange = props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id) - return pendingChange?.value - } - - const [hasBeenEdited, setHasBeenEdited] = React.useState( - getPendingState() !== undefined && getPendingState() !== props.userEditOperation.currentValues.value - ) - - const jsonSchema = Object.values(props.userEditOperation.schemas).find( - (layer) => layer.sourceLayerType === selectedSourceGroup - )?.schema - const selectedGroupSchema = jsonSchema ? JSONBlobParse(jsonSchema) : undefined - - React.useEffect(() => { - const pendingChange = props.pendingChanges.find((change) => change.operationId === props.userEditOperation.id) - - setSelectedSourceButton(pendingChange?.sourceLayerType || props.userEditOperation.currentValues.type) - - setSelectedValues(pendingChange?.value || props.userEditOperation.currentValues.value) - - setHasBeenEdited( - getPendingState() !== undefined && getPendingState() !== props.userEditOperation.currentValues.value - ) - }, [props.userEditOperation.id, props.pendingChanges]) - - const handleSourceChange = () => { - setSelectedValues(selectedValues) - setHasBeenEdited(true) - // Add to pending changes instead of executing immediately - props.setPendingChanges((prev) => { - const filtered = prev.filter( - (change) => - !( - change.operationId === props.userEditOperation.id && - change.userEditingType === UserEditingType.SOURCE_LAYER_FORM - ) - ) - // Only use the key,value pair from the selected source group: - const newKey = Object.keys(props.userEditOperation.schemas).find((key) => { - return props.userEditOperation.schemas[key].sourceLayerType === selectedSourceGroup }) - if (!newKey) return filtered - const newValue = selectedValues[newKey] - return [ - ...filtered, - { - operationId: props.userEditOperation.id, - userEditingType: UserEditingType.SOURCE_LAYER_FORM, - sourceLayerType: selectedSourceGroup, - value: { [newKey]: newValue }, + }, + [change] + ) + const onUpdate = useCallback( + (update: Record) => { + console.log(change.pieceTypeProperties.type, update) + setChange({ + ...change, + pieceTypeProperties: { + type: change.pieceTypeProperties.type, + value: update, }, - ] - }) - } + }) + }, + [change] + ) + const value = change.pieceTypeProperties.value return ( <>
- {Object.values(props.userEditOperation.schemas).map((group, index) => { + {Object.entries(properties.schema).map(([key, group]) => { return ( ) })} -
-
- {selectedGroupSchema && ( -
- {hasBeenEdited && } -
- -
+
+ {parsedSchema && ( +
+
)}
) } + +/** + * @todo - retrieve translationNamespaces for correct blueprint translations? + */ +function GlobalPropertiesEditor({ + schema, + change, + setChange, +}: { + schema: JSONBlob + change: PendingChange + setChange: React.Dispatch> +}): JSX.Element { + const parsedSchema = schema ? JSONBlobParse(schema) : undefined + const currentValue = change.globalProperties + + const onUpdate = useCallback( + (update: Record) => { + console.log('glob', update) + setChange({ + ...change, + globalProperties: update, + }) + }, + [change] + ) + + return ( +
+ {parsedSchema ? ( + + ) : ( +

No schema found

+ )} +
+ ) +} From aa35b1ba194c882cad0ac9da031bffa4c26464b2 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Mon, 2 Dec 2024 09:51:44 +0000 Subject: [PATCH 82/90] chore: update styles --- packages/shared-lib/src/lib/JSONSchemaUtil.ts | 1 + .../lib/forms/SchemaFormWithOverrides.tsx | 18 +- .../client/lib/forms/SchemaFormWithState.tsx | 2 +- .../webui/src/client/styles/_variables.scss | 1 + .../src/client/styles/propertiesPanel.scss | 216 +++++++++++------- .../webui/src/client/styles/rundownView.scss | 21 +- packages/webui/src/client/ui/RundownView.tsx | 5 +- .../webui/src/client/ui/Settings/Forms.scss | 8 + .../ui/UserEditOperations/PropertiesPanel.tsx | 111 ++++----- 9 files changed, 239 insertions(+), 144 deletions(-) diff --git a/packages/shared-lib/src/lib/JSONSchemaUtil.ts b/packages/shared-lib/src/lib/JSONSchemaUtil.ts index 02feb95420..49e6380de0 100644 --- a/packages/shared-lib/src/lib/JSONSchemaUtil.ts +++ b/packages/shared-lib/src/lib/JSONSchemaUtil.ts @@ -29,6 +29,7 @@ export enum SchemaFormUIField { * Currently only valid for: * - object properties. Valid values are 'json'. * - string properties. Valid values are 'base64-image'. + * - boolean properties. Valid values are 'switch'. */ DisplayType = 'ui:displayType', /** diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx index 31c02b0214..bf1a03a614 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx @@ -24,6 +24,7 @@ import { SchemaFormObjectTable } from './SchemaFormTable/ObjectTable' import { getSchemaUIField, SchemaFormUIField } from '@sofie-automation/blueprints-integration' import { SchemaFormSectionHeader } from './SchemaFormSectionHeader' import { Base64ImageInputControl } from '../Components/Base64ImageInput' +import { ToggleSwitchControl } from '../Components/ToggleSwitch' interface SchemaFormWithOverridesProps extends SchemaFormCommonProps { /** Base path of the schema within the document */ @@ -116,7 +117,11 @@ export function SchemaFormWithOverrides(props: Readonly case TypeName.Boolean: - return + if (getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'switch') { + return + } else { + return + } case TypeName.String: if (getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'base64-image') { return @@ -352,6 +357,17 @@ const BooleanFormWithOverrides = ({ commonAttrs }: Readonly) ) } +const SwitchFormWithOverrides = ({ commonAttrs }: Readonly) => { + return ( + + {(value, handleUpdate) => ( + // + + )} + + ) +} + const StringFormWithOverrides = ({ schema, commonAttrs }: Readonly) => { return ( diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx index e34530a3d3..77e84b75ee 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { OverrideOpHelperForItemContentsBatcher, WrappedOverridableItemNormal, diff --git a/packages/webui/src/client/styles/_variables.scss b/packages/webui/src/client/styles/_variables.scss index b7b6df1117..a325879f78 100644 --- a/packages/webui/src/client/styles/_variables.scss +++ b/packages/webui/src/client/styles/_variables.scss @@ -4,6 +4,7 @@ $fullscreen-controls__button--radius: 3.125rem; $statusbar-width: 3.6rem; $notification-center-width: 25rem; +$properties-panel-width: 550px; $browser-context: 16px; // Default browser font size in pixels diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 3c096419cb..67bf737c93 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -2,25 +2,23 @@ @import '_colorScheme'; @import '_variables'; - - .properties-panel { position: fixed; background: #7b7b7b; color: #000; - top: 50px; + top: 4rem; right: 0; - bottom: 10px; - width: calc(#{$notification-center-width} + 4.6875rem); + bottom: 0; + width: $properties-panel-width; z-index: 292; - transform: translateX(100%); - transition: transform 0.2s ease-out; - - // Add a class that will be applied when the component mounts - &.is-mounted { - transform: translateX(0%); - } + transform: translateX(100%); + transition: transform 0.2s ease-out; + + // Add a class that will be applied when the component mounts + &.is-mounted { + transform: translateX(0%); + } &::before { content: ' '; @@ -34,70 +32,115 @@ } .propertiespanel-pop-up { - background: #252525; + background: #2e2e2e; border-radius: 1px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); - margin: 1px; - height: 98.2%; - width: 87%; - - &:first-child { - margin-top: 0.9375rem; - } + height: 100%; + width: calc(100% - 3.75rem); + position: relative; - &:last-child { - margin-bottom: 0.9375rem; - } - - > .propertiespanel-pop-up_close { - position: absolute; - top: 25px; - right: 70px; - color: white; - } + display: flex; + flex-direction: column; > .propertiespanel-pop-up__header { - background: #3d3d3d; + background: #0a20ed; color: #ddd; - min-width: 2.5rem; - height: 2.5rem; - font-size: 1.2em; + // min-width: 2.5rem; + // height: 2.5rem; + max-width: 100%; - text-align: left; - padding-left: 1rem; display: flex; + padding: 1em; align-items: center; - gap: 0.5rem; + gap: 0.2em; + align-self: stretch; + + // text-align: left; + // padding-left: 1rem; + // display: flex; + // align-items: center; + // gap: 0.5rem; + + text-shadow: 0.5px 0.5px 8px rgba(0, 0, 0, 0.8); + font-family: Roboto; + font-size: 1em; + font-style: normal; + font-weight: 500; + line-height: normal; + letter-spacing: 0.5px; > .svg { - filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.15)); - width: 1.1em; - height: 1.1em; + // filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.15)); + width: 1em; + height: 1.2em; + flex-shrink: 0; + } + > .title { + flex-grow: 1; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + min-width: 0; + } + > .properties { + color: #fff; + font-family: Roboto; + font-size: 16px; + font-style: normal; + font-weight: 300; + line-height: normal; + letter-spacing: -0.1px; + flex-shrink: 0; + } + > .propertiespanel-pop-up_close { + height: 1em; + cursor: pointer; + margin-left: 1em; } } > .propertiespanel-pop-up__footer { flex: 1; - position: absolute; - left: 0; - right: 0; - bottom: 10px; + // position: absolute; + // left: 0; + // right: 0; + // bottom: 0; + flex: 0 0 0; + padding: 10px; display: flex; - justify-content: center; - align-items: center; - margin-right: 50px; + justify-content: space-between; + // align-items: center; + gap: 12px; - > .propertiespanel-pop-up__button { - background: #636363; - padding: 5px 5px 5px 5px; - gap: 10px; - border-radius: 5px; - border: 1px; - border: 1px solid #7F7F7F; - color: #ddd; + // margin-right: 50px; + + > .propertiespanel-pop-up__button-group { + display: inherit; + gap: inherit; + } + + > .propertiespanel-pop-up__button, + .propertiespanel-pop-up__button-group .propertiespanel-pop-up__button { + // background: #636363; + // padding: 5px 5px 5px 5px; + // gap: 10px; + // border-radius: 5px; + // border: 1px; + // border: 1px solid #7f7f7f; + // color: #ddd; + // margin: 10px auto; display: block; - margin: 10px auto; + + border-radius: 5px; + border: 1px solid #7f7f7f; + background: #636363; + padding: 10px; + + color: #dfdfdf; + font-size: 0.875em; + font-weight: 500; &:active { transform: scale(0.95); @@ -107,7 +150,7 @@ &:disabled { cursor: not-allowed; opacity: 0.3; - + &:active { transform: none; top: 0; @@ -128,21 +171,29 @@ margin-top: 2px; line-height: inherit; } + + &.start { + justify-self: start; + } + &.end { + justify-self: end; + } } } > .propertiespanel-pop-up__contents { flex: 1; - padding: 0.625rem 0.9375rem; padding: 0.525rem 0.6375rem; cursor: default; - + overflow: hidden auto; overflow-wrap: break-word; + flex: 1 0; + > hr { margin-left: 0px; width: 100%; - border-color: #7F7F7F; + border-color: #7f7f7f; } > .propertiespanel-pop-up__groupselector { @@ -150,7 +201,7 @@ flex-wrap: wrap; margin-top: 0.5em; margin-bottom: 0.5em; - + > .propertiespanel-pop-up__groupselector__button { @include item-type-colors(); @@ -161,20 +212,20 @@ gap: 10px; color: #ddd; opacity: 0.2; - } - > .propertiespanel-pop-up__groupselector__button-active { - @include item-type-colors(); + &.splits { + background: linear-gradient( + to right, + $segment-layer-background-camera 50%, + $segment-layer-background-remote 50.0001% + ); + } - width: 50px; - height: 30px; - border: 0px; - margin: 3px; - gap: 10px; - color: #fff; - opacity: 1; + &.active { + color: #fff; + opacity: 1; + } } - } > .propertiespanel-pop-up__action { @@ -191,32 +242,31 @@ display: flex; align-items: flex-start; gap: 8px; - + // Add positioning for the pencil icon > svg { - margin-top: 4px; - flex-shrink: 0; + margin-top: 4px; + flex-shrink: 0; } - + > .properties-panel-pop-up__form__schema { - border-color: pink; - border-width: 0px; - flex-grow: 1; + border-color: pink; + border-width: 0px; + flex-grow: 1; } } - + > .properties-panel-pop-up__has-been-edited { background-color: #ffffff16; border-radius: 8px; padding: 4px 4px; } - > .propertiespanel-pop-up__label { color: #ddd; } - > .propertiespanel-pop-up__select{ + > .propertiespanel-pop-up__select { margin-top: 10px; width: 100%; height: 3em; @@ -232,7 +282,7 @@ gap: 10px; border-radius: 5px; border: 1px; - border: 1px solid #7F7F7F; + border: 1px solid #7f7f7f; color: #ddd; &:active { diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index 2db0a2c0b7..e94d593d33 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -158,6 +158,16 @@ $break-width: 35rem; transition: 0s padding-right 1s; } } + + &.properties-panel-open { + padding-right: $properties-panel-width; + transition: 0s padding-right 1s; + + > .header .rundown-overview { + padding-right: calc(#{$properties-panel-width} + 1.5em); + transition: 0s padding-right 1s; + } + } } body.vertical-overflow-only { @@ -1399,12 +1409,13 @@ svg.icon { &.quickloop-start { left: -2px; - &::before, &::after { + &::before, + &::after { z-index: 1; margin-left: 5px; border-left-color: white; } - + .segment-timeline__part__nextline__label { z-index: 5; left: 5px; @@ -1586,7 +1597,6 @@ svg.icon { } } - &:not(.live) { .segment-timeline__part__nextline.auto-next:not(.segment-timeline__part__nextline--endline), .segment-timeline__part__nextline.invalid:not(.segment-timeline__part__nextline--endline) { @@ -1832,7 +1842,8 @@ svg.icon { right: 0; } - .segment-timeline__part__quickloop-start, .segment-timeline__part__quickloop-end { + .segment-timeline__part__quickloop-start, + .segment-timeline__part__quickloop-end { background: $segment-background-color; padding: 0 0.3em; margin-bottom: -2px; @@ -1849,7 +1860,7 @@ svg.icon { .segment-timeline__part__quickloop-end { padding-right: 0.6em; - margin-left: auto + margin-left: auto; } } diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 985402f2cd..e07dc11219 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -2969,10 +2969,9 @@ const RundownViewContent = translateWithTracker 0, + 'notification-center-open': this.state.isNotificationsCenterOpen !== undefined, 'rundown-view--studio-mode': this.state.studioMode, + 'properties-panel-open': selectionContext.listSelectedElements().length > 0, })} style={this.getStyle()} onWheelCapture={this.onWheel} diff --git a/packages/webui/src/client/ui/Settings/Forms.scss b/packages/webui/src/client/ui/Settings/Forms.scss index 9d5849c0aa..a31b5f5453 100644 --- a/packages/webui/src/client/ui/Settings/Forms.scss +++ b/packages/webui/src/client/ui/Settings/Forms.scss @@ -7,6 +7,14 @@ &:not(:first-child) { margin-top: 20px; } + + &.form-dark { + color: white; + + > .text-input.bghl { + color: black !important; + } + } } .properties-grid > :not(.field) { diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index f07de4a68d..6fda0532ae 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -112,6 +112,11 @@ export function PropertiesPanel(): JSX.Element { ) } + const handleCancel = () => { + setPendingChange(undefined) + clearSelections() + } + const userEditOperations = selectedElement?.type === 'part' ? part?.userEditOperations @@ -135,59 +140,64 @@ export function PropertiesPanel(): JSX.Element { return (
-
- +
+ {userEditOperations && + userEditOperations.map((operation) => { + if (operation.type !== UserEditingType.ACTION || !operation.svgIcon || !operation.isActive) return null + return ( +
+ ) + })} +
{title}
+ {t('Properties')} +
+ +
+
+ +
+ {userEditProperties?.pieceTypeProperties && ( + + )} + {userEditProperties?.globalProperties && ( + + )}
- <> -
- {userEditOperations && - userEditOperations.map((operation) => { - if (operation.type !== UserEditingType.ACTION || !operation.svgIcon || !operation.isActive) return null - return ( -
- ) - })} - {title} -
-
- {userEditProperties?.pieceTypeProperties && ( - - )} - {userEditProperties?.globalProperties && ( - - )} -
-
- +
+ + +
@@ -243,10 +253,9 @@ function PropertiesEditor({ return (

{parsedSchema && ( -
+
) : ( -

No schema found

+ <> )}
) From ea2fb4ae9a1d9948313ceb5e76835bf34dd24135 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Mon, 2 Dec 2024 15:26:19 +0000 Subject: [PATCH 83/90] chore: add actions back into property panel --- .../blueprints-integration/src/userEditing.ts | 3 +- .../src/client/styles/propertiesPanel.scss | 69 ++++-------------- .../ui/UserEditOperations/PropertiesPanel.tsx | 70 ++++++++++++++++++- 3 files changed, 82 insertions(+), 60 deletions(-) diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index 8807c8fe3e..4160c81846 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -125,7 +125,6 @@ export interface UserEditingProperties { * A list of id's of operations to be exposed on the properties panel as buttons. These operations * must be available on the element * - * note - perhaps these should have their own full definitions? */ - operations?: string[] + operations?: UserEditingDefinitionAction[] } diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 67bf737c93..d5d82c12bf 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -123,14 +123,6 @@ > .propertiespanel-pop-up__button, .propertiespanel-pop-up__button-group .propertiespanel-pop-up__button { - // background: #636363; - // padding: 5px 5px 5px 5px; - // gap: 10px; - // border-radius: 5px; - // border: 1px; - // border: 1px solid #7f7f7f; - // color: #ddd; - // margin: 10px auto; display: block; border-radius: 5px; @@ -165,6 +157,11 @@ height: 1em; } + svg { + width: 1.3em; + height: 0.875em; + } + .label { margin-left: 10px; margin-right: 10px; @@ -235,67 +232,29 @@ } > .properties-panel-pop-up__form { - margin-top: 15px; - color: #ddd; - padding: 4px 4px; - position: relative; - display: flex; - align-items: flex-start; - gap: 8px; - - // Add positioning for the pencil icon - > svg { - margin-top: 4px; - flex-shrink: 0; - } - - > .properties-panel-pop-up__form__schema { - border-color: pink; - border-width: 0px; - flex-grow: 1; - } - } - - > .properties-panel-pop-up__has-been-edited { - background-color: #ffffff16; - border-radius: 8px; - padding: 4px 4px; - } - - > .propertiespanel-pop-up__label { color: #ddd; } - > .propertiespanel-pop-up__select { - margin-top: 10px; - width: 100%; - height: 3em; - background: #232323; - margin-bottom: 0.5em; - color: #ddd; - } - - > .propertiespanel-pop-up__button { - margin-top: 10px; + .propertiespanel-pop-up__button { + // margin-top: 10px; background: #636363; - padding: 5px 5px 5px 5px; + padding: 10px; gap: 10px; border-radius: 5px; - border: 1px; border: 1px solid #7f7f7f; - color: #ddd; + color: #dfdfdf; + + font-size: 0.875em; + font-weight: 500; &:active { transform: scale(0.95); top: 2px; } - > svg { - margin-top: -0.1em; - vertical-align: middle; - margin-right: -0.4em; + svg { width: 1em; - height: 1em; + height: 0.875em; } .label { diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 6fda0532ae..21d55d9e89 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -7,6 +7,7 @@ import { JSONBlob, JSONBlobParse, JSONSchema, + UserEditingDefinitionAction, UserEditingProperties, UserEditingSourceLayer, UserEditingType, @@ -23,6 +24,8 @@ import { RundownUtils } from '../../lib/rundown' import * as CoreIcon from '@nrk/core-icons/jsx' import { useCallback } from 'react' import { SchemaFormWithState } from '../../lib/forms/SchemaFormWithState' +import { t } from 'i18next' +import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' type PendingChange = DefaultUserOperationEditProperties['payload'] @@ -117,6 +120,25 @@ export function PropertiesPanel(): JSX.Element { clearSelections() } + const executeAction = (e: React.MouseEvent, id: string) => { + if (!rundownId || !selectedElement) return + doUserAction(t, e, UserAction.EXECUTE_USER_OPERATION, (e, ts) => + MeteorCall.userAction.executeUserChangeOperation( + e, + ts, + rundownId, + { + segmentExternalId: segment?.externalId, + partExternalId: part?.externalId, + pieceExternalId: undefined, + }, + { + id, + } + ) + ) + } + const userEditOperations = selectedElement?.type === 'part' ? part?.userEditOperations @@ -176,6 +198,9 @@ export function PropertiesPanel(): JSX.Element { setChange={setPendingChange} /> )} + {userEditProperties?.operations && ( + + )}
@@ -184,6 +209,14 @@ export function PropertiesPanel(): JSX.Element { onClick={handleRevertChanges} // disabled={!hasPendingChanges} > + + + + + {t('Restore from NRCS')}
@@ -233,7 +266,6 @@ function PropertiesEditor({ ) const onUpdate = useCallback( (update: Record) => { - console.log(change.pieceTypeProperties.type, update) setChange({ ...change, pieceTypeProperties: { @@ -269,7 +301,7 @@ function PropertiesEditor({

{parsedSchema && ( -
+
) => { - console.log('glob', update) setChange({ ...change, globalProperties: update, @@ -326,3 +357,36 @@ function GlobalPropertiesEditor({
) } + +function ActionList({ + actions, + executeAction, +}: { + actions: UserEditingDefinitionAction[] + executeAction: (e: any, id: string) => void +}) { + const { t } = useTranslation() + + return ( +
+ {actions.map((action) => ( + + ))} +
+ ) +} From a671baa187e461e146a1047a37940dda8c756a1e Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Thu, 5 Dec 2024 10:42:19 +0000 Subject: [PATCH 84/90] chore: support translations for properties panel --- .../src/documents/part.ts | 2 +- .../blueprints-integration/src/userEditing.ts | 4 +- packages/corelib/src/dataModel/Part.ts | 4 +- packages/corelib/src/dataModel/Segment.ts | 4 +- .../src/dataModel/UserEditingDefinitions.ts | 7 +- .../docs/for-developers/json-config-schema.md | 7 +- .../job-worker/src/blueprints/context/lib.ts | 66 ++++++++++++++++++- .../src/ingest/generationSegment.ts | 8 ++- .../src/client/styles/propertiesPanel.scss | 3 +- .../ui/UserEditOperations/PropertiesPanel.tsx | 43 ++++++++---- 10 files changed, 119 insertions(+), 29 deletions(-) diff --git a/packages/blueprints-integration/src/documents/part.ts b/packages/blueprints-integration/src/documents/part.ts index 0807a2183b..31b030869b 100644 --- a/packages/blueprints-integration/src/documents/part.ts +++ b/packages/blueprints-integration/src/documents/part.ts @@ -93,7 +93,7 @@ export interface IBlueprintMutatablePart + userEditProperties?: UserEditingProperties } export interface HackPartMediaObjectSubscription { diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index 4160c81846..2dbfc2c818 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -74,9 +74,10 @@ export enum UserEditingType { } export interface UserEditingSourceLayer { - sourceLayerLabel: string // translate? take from type? + sourceLayerLabel: string sourceLayerType: SourceLayerType schema: JSONBlob + defaultValue?: Record } export enum UserEditingButtonType { @@ -124,7 +125,6 @@ export interface UserEditingProperties { /** * A list of id's of operations to be exposed on the properties panel as buttons. These operations * must be available on the element - * */ operations?: UserEditingDefinitionAction[] } diff --git a/packages/corelib/src/dataModel/Part.ts b/packages/corelib/src/dataModel/Part.ts index a5bf7a1c31..9712dbe43d 100644 --- a/packages/corelib/src/dataModel/Part.ts +++ b/packages/corelib/src/dataModel/Part.ts @@ -3,7 +3,7 @@ import { ITranslatableMessage } from '../TranslatableMessage' import { PartId, RundownId, SegmentId } from './Ids' import { PartNote } from './Notes' import { ReadonlyDeep } from 'type-fest' -import { CoreUserEditingDefinition, UserEditingProperties } from './UserEditingDefinitions' +import { CoreUserEditingDefinition, CoreUserEditingProperties } from './UserEditingDefinitions' export interface PartInvalidReason { message: ITranslatableMessage @@ -46,7 +46,7 @@ export interface DBPart extends Omit { * 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 + userEditProperties?: CoreUserEditingProperties } export function isPartPlayable(part: Pick, 'invalid' | 'floated'>): boolean { diff --git a/packages/corelib/src/dataModel/Segment.ts b/packages/corelib/src/dataModel/Segment.ts index 580e2daefd..7d756380c5 100644 --- a/packages/corelib/src/dataModel/Segment.ts +++ b/packages/corelib/src/dataModel/Segment.ts @@ -1,7 +1,7 @@ import { SegmentDisplayMode, SegmentTimingInfo } from '@sofie-automation/blueprints-integration' import { SegmentId, RundownId } from './Ids' import { SegmentNote } from './Notes' -import { CoreUserEditingDefinition, UserEditingProperties } from './UserEditingDefinitions' +import { CoreUserEditingDefinition, CoreUserEditingProperties } from './UserEditingDefinitions' export enum SegmentOrphanedReason { /** Segment is deleted from the NRCS but we still need it */ @@ -56,5 +56,5 @@ export interface DBSegment { * 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 + userEditProperties?: CoreUserEditingProperties } diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index 31bcf1bf08..52509e994b 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -67,7 +67,7 @@ export interface CoreUserEditingDefinitionSourceLayerForm { } } -export interface UserEditingProperties { +export interface CoreUserEditingProperties { /** * These properties are dependent on the (primary) piece type, the user will get the option * to select the type of piece (from the SourceLayerTypes i.e. Camera or Split etc.) and then @@ -106,5 +106,8 @@ export interface UserEditingProperties { * * note - perhaps these should have their own full definitions? */ - operations?: string[] + operations?: CoreUserEditingDefinitionAction[] + + /** Translation namespaces to use when rendering this form */ + translationNamespaces: string[] } diff --git a/packages/documentation/docs/for-developers/json-config-schema.md b/packages/documentation/docs/for-developers/json-config-schema.md index b56e6e6ee7..862a5dd31f 100644 --- a/packages/documentation/docs/for-developers/json-config-schema.md +++ b/packages/documentation/docs/for-developers/json-config-schema.md @@ -43,7 +43,12 @@ If an integer property, whether to treat it as zero-based ### `ui:displayType` Override the presentation with a special mode. -Currently only valid for string properties. Valid values are 'json'. + +Currently only valid for: + +- object properties. Valid values are 'json'. +- string properties. Valid values are 'base64-image'. +- boolean properties. Valid values are 'switch'. ### `tsEnumNames` diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index a9c84085e6..fa22ee83e2 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -14,7 +14,7 @@ import { CoreUserEditingDefinitionAction, CoreUserEditingDefinitionForm, CoreUserEditingDefinitionSourceLayerForm, - UserEditingProperties, + CoreUserEditingProperties, } from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { assertNever, clone, Complete, literal, omit } from '@sofie-automation/corelib/dist/lib' @@ -59,6 +59,7 @@ import { UserEditingDefinitionAction, UserEditingDefinitionForm, UserEditingDefinitionSourceLayerForm, + UserEditingProperties, UserEditingType, } from '@sofie-automation/blueprints-integration/dist/userEditing' import type { PlayoutMutatablePart } from '../../playout/model/PlayoutPartInstanceModel' @@ -283,7 +284,7 @@ export function convertPartToBlueprints(part: ReadonlyDeep): IBlueprintP part.hackListenToMediaObjectUpdates ), userEditOperations: translateUserEditsToBlueprint(part.userEditOperations), - userEditProperties: clone(part.userEditProperties), + userEditProperties: translateUserEditPropertiesToBlueprint(part.userEditProperties), } return obj @@ -352,7 +353,7 @@ export function convertSegmentToBlueprints(segment: ReadonlyDeep): IB showShelf: segment.showShelf, segmentTiming: segment.segmentTiming, userEditOperations: translateUserEditsToBlueprint(segment.userEditOperations), - userEditProperties: clone(segment.userEditProperties), + userEditProperties: translateUserEditPropertiesToBlueprint(segment.userEditProperties), } return obj @@ -544,6 +545,30 @@ function translateUserEditsToBlueprint( ) } +function translateUserEditPropertiesToBlueprint( + props: ReadonlyDeep | undefined +): UserEditingProperties | undefined { + if (!props) return undefined + + return { + globalProperties: props.globalProperties, + pieceTypeProperties: props.pieceTypeProperties, + + operations: props.operations?.map( + (userEdit) => + ({ + type: UserEditingType.ACTION, + id: userEdit.id, + label: omit(userEdit.label, 'namespaces'), + svgIcon: userEdit.svgIcon, + svgIconInactive: userEdit.svgIconInactive, + isActive: userEdit.isActive, + buttonType: userEdit.buttonType, + } satisfies Complete) + ), + } +} + export function translateUserEditsFromBlueprint( userEdits: UserEditingDefinition[] | undefined, blueprintIds: BlueprintId[] @@ -589,6 +614,33 @@ export function translateUserEditsFromBlueprint( ) } +export function translateUserEditPropertiesFromBlueprint( + props: UserEditingProperties | undefined, + blueprintIds: BlueprintId[] +): CoreUserEditingProperties | undefined { + if (!props) return undefined + + return { + globalProperties: clone(props.globalProperties), + pieceTypeProperties: clone(props.pieceTypeProperties), + + operations: props.operations?.map( + (userEdit) => + ({ + type: UserEditingType.ACTION, + id: userEdit.id, + label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds), + svgIcon: userEdit.svgIcon, + svgIconInactive: userEdit.svgIconInactive, + isActive: userEdit.isActive, + buttonType: userEdit.buttonType, + } satisfies Complete) + ), + + translationNamespaces: blueprintIds.map((id) => `blueprint_${id}`), + } +} + export function convertPartialBlueprintMutablePartToCore( updatePart: Partial, blueprintId: BlueprintId @@ -606,5 +658,13 @@ export function convertPartialBlueprintMutablePartToCore( delete playoutUpdatePart.userEditOperations } + if ('userEditProperties' in updatePart) { + playoutUpdatePart.userEditProperties = translateUserEditPropertiesFromBlueprint(updatePart.userEditProperties, [ + blueprintId, + ]) + } else { + delete playoutUpdatePart.userEditOperations + } + return playoutUpdatePart } diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index 2898cef866..5842047f3c 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -24,7 +24,7 @@ import { IngestReplacePartType, IngestSegmentModel } from './model/IngestSegment import { ReadonlyDeep } from 'type-fest' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { WrappedShowStyleBlueprint } from '../blueprints/cache' -import { translateUserEditsFromBlueprint } from '../blueprints/context/lib' +import { translateUserEditPropertiesFromBlueprint, translateUserEditsFromBlueprint } from '../blueprints/context/lib' async function getWatchedPackagesHelper( context: JobContext, @@ -293,6 +293,9 @@ function updateModelWithGeneratedSegment( userEditOperations: translateUserEditsFromBlueprint(blueprintSegment.segment.userEditOperations, [ blueprintId, ]), + userEditProperties: translateUserEditPropertiesFromBlueprint(blueprintSegment.segment.userEditProperties, [ + blueprintId, + ]), }) ) @@ -376,6 +379,9 @@ function updateModelWithGeneratedPart( } : undefined, userEditOperations: translateUserEditsFromBlueprint(blueprintPart.part.userEditOperations, [blueprintId]), + userEditProperties: translateUserEditPropertiesFromBlueprint(blueprintPart.part.userEditProperties, [ + blueprintId, + ]), }) // Update pieces diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index d5d82c12bf..10eb99c1eb 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -96,8 +96,9 @@ } > .propertiespanel-pop-up_close { height: 1em; - cursor: pointer; margin-left: 1em; + background-color: unset; + border: none; } } diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 21d55d9e89..1c784a6a41 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -22,9 +22,8 @@ import { useSelection } from '../RundownView/SelectedElementsContext' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { RundownUtils } from '../../lib/rundown' import * as CoreIcon from '@nrk/core-icons/jsx' -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { SchemaFormWithState } from '../../lib/forms/SchemaFormWithState' -import { t } from 'i18next' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' type PendingChange = DefaultUserOperationEditProperties['payload'] @@ -178,9 +177,13 @@ export function PropertiesPanel(): JSX.Element { })}
{title}
{t('Properties')} -
+
+
@@ -189,6 +192,7 @@ export function PropertiesPanel(): JSX.Element { properties={userEditProperties.pieceTypeProperties} change={change} setChange={setPendingChange} + translationNamespace={userEditProperties.translationNamespaces} /> )} {userEditProperties?.globalProperties && ( @@ -196,6 +200,7 @@ export function PropertiesPanel(): JSX.Element { schema={userEditProperties.globalProperties.schema} change={change} setChange={setPendingChange} + translationNamespace={userEditProperties.translationNamespaces} /> )} {userEditProperties?.operations && ( @@ -206,8 +211,8 @@ export function PropertiesPanel(): JSX.Element {
- @@ -241,16 +252,21 @@ function PropertiesEditor({ properties, change, setChange, + translationNamespace, }: { properties: UserEditingProperties['pieceTypeProperties'] change: PendingChange setChange: React.Dispatch> + translationNamespace: string[] }): JSX.Element { if (!properties) return <> const selectedGroupId = change.pieceTypeProperties.type const selectedGroupSchema = properties.schema[selectedGroupId]?.schema - const parsedSchema = selectedGroupSchema ? JSONBlobParse(selectedGroupSchema) : undefined + const parsedSchema = useMemo( + () => (selectedGroupSchema ? JSONBlobParse(selectedGroupSchema) : undefined), + [selectedGroupSchema] + ) const updateGroup = useCallback( (key: string) => { @@ -258,7 +274,7 @@ function PropertiesEditor({ ...change, pieceTypeProperties: { type: key, - value: {}, // todo - take defaults + value: properties.schema[key]?.defaultValue ?? {}, }, }) }, @@ -307,7 +323,7 @@ function PropertiesEditor({ schema={parsedSchema} object={value} onUpdate={onUpdate} - translationNamespaces={[]} + translationNamespaces={translationNamespace} />
)} @@ -316,17 +332,16 @@ function PropertiesEditor({ ) } -/** - * @todo - retrieve translationNamespaces for correct blueprint translations? - */ function GlobalPropertiesEditor({ schema, change, setChange, + translationNamespace, }: { schema: JSONBlob change: PendingChange setChange: React.Dispatch> + translationNamespace: string[] }): JSX.Element { const parsedSchema = schema ? JSONBlobParse(schema) : undefined const currentValue = change.globalProperties @@ -349,7 +364,7 @@ function GlobalPropertiesEditor({ schema={parsedSchema} object={currentValue} onUpdate={onUpdate} - translationNamespaces={[]} + translationNamespaces={translationNamespace} /> ) : ( <> @@ -371,7 +386,7 @@ function ActionList({
{actions.map((action) => ( - ) - }} - + {props.isUserEditsEnabled && ( + + )}
) } + +function PropertiesPanelToggle(props: { isNotificationCenterOpen: NoticeLevel | undefined }) { + return ( + + {(context) => { + const isOpen = context.listSelectedElements().length > 0 && !props.isNotificationCenterOpen + return ( + + ) + }} + + ) +} diff --git a/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx b/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx index d1ea0f4f63..02232a5429 100644 --- a/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx +++ b/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useState } from 'react' import { AdLibActionId, PartId, @@ -8,6 +8,12 @@ import { SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { Tracker } from 'meteor/tracker' +import { Pieces, Segments } from '../../collections' +import { UIParts } from '../Collections' interface RundownElement { type: 'rundown' @@ -40,7 +46,7 @@ interface AdlibActionElement { } // Union types for all possible elements -type SelectedElement = +export type SelectedElement = | RundownElement | SegmentElement | PartElement @@ -118,18 +124,18 @@ const defaultSelectionContext: SelectionContextType = { getSelectedCount: () => 0, } -export const SelectedElementsContext = React.createContext(defaultSelectionContext) +export const SelectedElementsContext = createContext(defaultSelectionContext) export const SelectedElementProvider: React.FC<{ children: React.ReactNode maxSelections?: number // Optional prop to limit maximum selections }> = ({ children, maxSelections = 10 }) => { - const [selectedElements, dispatch] = React.useReducer( + const [selectedElements, dispatch] = useReducer( (state: Map, action: SelectionAction) => selectionReducer(state, action, maxSelections), new Map() ) - const value = React.useMemo( + const value = useMemo( () => ({ isSelected: (elementId: ElementId) => { return selectedElements.has(elementId) @@ -169,20 +175,58 @@ export const SelectedElementProvider: React.FC<{ } // Custom hook for using the selection context -export const useSelection = (): SelectionContextType => { - const context = React.useContext(SelectedElementsContext) - if (!context) { - throw new Error('useSelection must be used within a SelectedElementProvider') - } +export const useSelectedElementsContext = (): SelectionContextType => { + const context = useContext(SelectedElementsContext) + return context } // Helper hook for common selection patterns export const useElementSelection = (element: SelectedElement): { isSelected: boolean; toggleSelection: () => void } => { - const { isSelected, toggleSelection } = useSelection() + const { isSelected, toggleSelection } = useSelectedElementsContext() + + return { + isSelected: useMemo(() => isSelected(element.elementId), [isSelected, element.elementId]), + toggleSelection: useCallback(() => toggleSelection(element), [toggleSelection, element]), + } +} + +export function useSelectedElements( + selectedElement: SelectedElement, + clearPendingChange: () => void +): { + piece: Piece | undefined + part: DBPart | undefined + segment: DBSegment | undefined + rundownId: RundownId | undefined +} { + const [piece, setPiece] = useState(undefined) + const [part, setPart] = useState(undefined) + const [segment, setSegment] = useState(undefined) + const rundownId = piece ? piece.startRundownId : part ? part.rundownId : segment?.rundownId + + useEffect(() => { + clearPendingChange() // element id changed so any pending change is for an old element + + const pieceComputation = Tracker.nonreactive(() => + Tracker.autorun(() => { + const piece = Pieces.findOne(selectedElement.elementId) + const part = UIParts.findOne({ _id: piece ? piece.startPartId : selectedElement?.elementId }) + const segment = Segments.findOne({ _id: part ? part.segmentId : selectedElement?.elementId }) + + setPiece(piece) + setPart(part) + setSegment(segment) + }) + ) + + return () => pieceComputation.stop() + }, [selectedElement?.elementId]) return { - isSelected: React.useMemo(() => isSelected(element.elementId), [isSelected, element.elementId]), - toggleSelection: React.useCallback(() => toggleSelection(element), [toggleSelection, element]), + piece, + part, + segment, + rundownId, } } diff --git a/packages/webui/src/client/ui/RundownView/__tests__/selectedElementsContext.test.tsx b/packages/webui/src/client/ui/RundownView/__tests__/selectedElementsContext.test.tsx index 401f3235a6..d17952dfc8 100644 --- a/packages/webui/src/client/ui/RundownView/__tests__/selectedElementsContext.test.tsx +++ b/packages/webui/src/client/ui/RundownView/__tests__/selectedElementsContext.test.tsx @@ -1,7 +1,7 @@ import React from 'react' // eslint-disable-next-line node/no-unpublished-import import { renderHook, act } from '@testing-library/react' -import { SelectedElementProvider, useSelection, useElementSelection } from '../SelectedElementsContext' +import { SelectedElementProvider, useSelectedElementsContext, useElementSelection } from '../SelectedElementsContext' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { RundownId, SegmentId, PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -27,14 +27,14 @@ describe('SelectedElementProvider', () => { ) test('init with no selections', () => { - const { result } = renderHook(() => useSelection(), { wrapper }) + const { result } = renderHook(() => useSelectedElementsContext(), { wrapper }) expect(result.current.listSelectedElements().length).toBe(0) expect(result.current.getSelectedCount()).toBe(0) }) test('clearAndSetSelection', () => { - const { result } = renderHook(() => useSelection(), { wrapper }) + const { result } = renderHook(() => useSelectedElementsContext(), { wrapper }) const element1 = createRundownElement('rundown1') const element2 = createRundownElement('rundown2') @@ -53,7 +53,7 @@ describe('SelectedElementProvider', () => { }) test('toggleSelection', () => { - const { result } = renderHook(() => useSelection(), { wrapper }) + const { result } = renderHook(() => useSelectedElementsContext(), { wrapper }) const element = createSegmentElement('segment1') act(() => { @@ -71,7 +71,7 @@ describe('SelectedElementProvider', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) - const { result } = renderHook(() => useSelection(), { wrapper }) + const { result } = renderHook(() => useSelectedElementsContext(), { wrapper }) const elements = [ createRundownElement('rundown1'), @@ -92,7 +92,7 @@ describe('SelectedElementProvider', () => { }) test('clearSelections removes all selections', () => { - const { result } = renderHook(() => useSelection(), { wrapper }) + const { result } = renderHook(() => useSelectedElementsContext(), { wrapper }) act(() => { result.current.addSelection(createRundownElement('rundown1')) diff --git a/packages/webui/src/client/ui/SegmentContainer/PieceElement.tsx b/packages/webui/src/client/ui/SegmentContainer/PieceElement.tsx index cc14b3b9e2..99cdf9f13e 100644 --- a/packages/webui/src/client/ui/SegmentContainer/PieceElement.tsx +++ b/packages/webui/src/client/ui/SegmentContainer/PieceElement.tsx @@ -45,6 +45,7 @@ export const PieceElement = React.forwardRef
- this.props.onEditSegmentProps(part.instance.segmentId)}> + this.props.onEditSegmentProps(part.instance.segmentId)}> {t('Edit Segment Properties')} @@ -192,10 +197,10 @@ export const SegmentContextMenu = withTranslation()( {this.props.enableUserEdits && ( <>
- this.props.onEditSegmentProps(part.instance.segmentId)}> + this.props.onEditSegmentProps(part.instance.segmentId)}> {t('Edit Segment Properties')} - this.props.onEditPartProps(part.instance.part._id)}> + this.props.onEditPartProps(part.instance.part._id)}> {t('Edit Part Properties')} diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 17277ef407..2c52980b62 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -57,7 +57,6 @@ import { logger } from '../../lib/logging' import * as RundownResolver from '../../lib/RundownResolver' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { SelectedElementsContext } from '../RundownView/SelectedElementsContext' -import classNames from 'classnames' interface IProps { id: string @@ -93,8 +92,8 @@ interface IProps { onFollowLiveLine?: (state: boolean, event: any) => void onShowEntireSegment?: (event: React.MouseEvent | undefined) => void onContextMenu?: (contextMenuContext: IContextMenuContext) => void - onItemClick?: (piece: PieceUi, e: React.MouseEvent) => void - onPieceDoubleClick?: (item: PieceUi, e: React.MouseEvent) => void + onPieceClick?: (piece: PieceUi, e: React.MouseEvent) => void + onPieceDoubleClick?: (piece: PieceUi, e: React.MouseEvent) => void onHeaderNoteClick?: (segmentId: SegmentId, level: NoteSeverity) => void onSwitchViewMode?: (newViewMode: SegmentViewMode) => void segmentRef?: (el: SegmentTimelineClass, segmentId: SegmentId) => void @@ -103,7 +102,6 @@ interface IProps { showCountdownToSegment: boolean showDurationSourceLayers?: Set fixedSegmentDuration: boolean | undefined - // isSelected: boolean } interface IStateHeader { timelineWidth: number @@ -790,7 +788,7 @@ export class SegmentTimelineClass extends React.Component

{this.props.segment.name} diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index 449c9973b4..0895504afa 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -664,7 +664,7 @@ const SegmentTimelineContainerContent = withResolvedSegment( maxTimeScale={this.state.maxTimeScale} onRecalculateMaxTimeScale={this.updateMaxTimeScale} showingAllSegment={this.state.showingAllSegment} - onItemClick={this.props.onPieceClick} + onPieceClick={this.props.onPieceClick} onPieceDoubleClick={this.props.onPieceDoubleClick} onCollapseOutputToggle={this.onCollapseOutputToggle} collapsedOutputs={this.state.collapsedOutputs} @@ -694,7 +694,6 @@ const SegmentTimelineContainerContent = withResolvedSegment( showCountdownToSegment={this.props.showCountdownToSegment} fixedSegmentDuration={this.props.fixedSegmentDuration} showDurationSourceLayers={this.props.showDurationSourceLayers} - //isSelected={this.props.isSelected} /> )} {this.props.segmentui.showShelf && this.props.adLibSegmentUi && ( diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 72605c15bf..671766f3a2 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -15,60 +15,24 @@ import { import { literal } from '@sofie-automation/corelib/dist/lib' import classNames from 'classnames' import { useTranslation } from 'react-i18next' -import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { Pieces, Segments } from '../../collections' -import { UIParts } from '../Collections' -import { useSelection } from '../RundownView/SelectedElementsContext' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { useSelectedElements, useSelectedElementsContext } from '../RundownView/SelectedElementsContext' import { RundownUtils } from '../../lib/rundown' import * as CoreIcon from '@nrk/core-icons/jsx' -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import { SchemaFormWithState } from '../../lib/forms/SchemaFormWithState' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' type PendingChange = DefaultUserOperationEditProperties['payload'] export function PropertiesPanel(): JSX.Element { - const { listSelectedElements, clearSelections } = useSelection() + const { listSelectedElements, clearSelections } = useSelectedElementsContext() const selectedElement = listSelectedElements()?.[0] const { t } = useTranslation() - const [pendingChange, setPendingChange] = React.useState(undefined) + const [pendingChange, setPendingChange] = useState(undefined) const hasPendingChanges = !!pendingChange - const [isAnimatedIn, setIsAnimatedIn] = React.useState(false) - React.useEffect(() => { - const timer = setTimeout(() => { - setIsAnimatedIn(true) - }, 10) - return () => clearTimeout(timer) - }, []) - - React.useEffect(() => { - return () => { - Array.from(document.querySelectorAll('.propertiespanel-pop-up.is-highlighted')).forEach((element: Element) => { - if (element instanceof HTMLElement) { - element.style.animationName = '' - } - }) - } - }, []) - - const piece = useTracker(() => { - setPendingChange(undefined) - return Pieces.findOne(selectedElement?.elementId) - }, [selectedElement?.elementId]) - - const part = useTracker(() => { - setPendingChange(undefined) - return UIParts.findOne({ _id: selectedElement?.elementId }) - }, [selectedElement?.elementId]) - - const segment: DBSegment | undefined = useTracker( - () => Segments.findOne({ _id: part ? part.segmentId : selectedElement?.elementId }), - [selectedElement?.elementId, part?.segmentId] - ) - const rundownId = piece ? piece.startRundownId : part ? part.rundownId : segment?.rundownId + const { piece, part, segment, rundownId } = useSelectedElements(selectedElement, () => setPendingChange(undefined)) const handleCommitChanges = async (e: React.MouseEvent) => { if (!rundownId || !selectedElement || !pendingChange) return @@ -85,7 +49,7 @@ export function PropertiesPanel(): JSX.Element { { segmentExternalId: segment?.externalId, partExternalId: part?.externalId, - pieceExternalId: undefined, + pieceExternalId: piece?.externalId, }, literal({ id: DefaultUserOperationsTypes.UPDATE_PROPS, @@ -111,9 +75,9 @@ export function PropertiesPanel(): JSX.Element { }, { id: - selectedElement.type === 'partInstance' - ? DefaultUserOperationsTypes.REVERT_PART - : DefaultUserOperationsTypes.REVERT_SEGMENT, + selectedElement.type === 'segment' + ? DefaultUserOperationsTypes.REVERT_SEGMENT + : DefaultUserOperationsTypes.REVERT_PART, } ) ) @@ -174,7 +138,7 @@ export function PropertiesPanel(): JSX.Element { : undefined return ( -
+
{userEditOperations && @@ -226,7 +190,7 @@ export function PropertiesPanel(): JSX.Element {
+
@@ -339,7 +340,7 @@ function GlobalPropertiesEditor({ ) return ( -
+
{parsedSchema ? ( ({ return { t: (str: string) => str, i18n: { - changeLanguage: () => new Promise(() => {}), + changeLanguage: () => + new Promise(() => { + // satisfy linter - by making it uglier? ¯\_(ツ)_/¯ + }), }, } }, initReactI18next: { type: '3rdParty', - init: () => {}, + init: () => { + // satisfy linter - by making it uglier? ¯\_(ツ)_/¯ + }, }, })) @@ -164,6 +169,9 @@ jest.mock('../../../lib/meteorApi', () => ({ jest.mock('../../../lib/forms/SchemaFormInPlace', () => ({ SchemaFormInPlace: () =>
Schema Form
, })) +jest.mock('../../../lib/forms/SchemaFormWithState', () => ({ + SchemaFormWithState: () =>
Schema Form
, +})) describe('PropertiesPanel', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -206,6 +214,18 @@ describe('PropertiesPanel', () => { svgIcon: '', }, ], + userEditProperties: { + operations: [ + { + id: 'operation1', + label: { key: 'TEST_LABEL', namespaces: ['blueprint_main-showstyle'] }, + type: UserEditingType.ACTION, + isActive: false, + svgIcon: '', + }, + ], + translationNamespaces: ['blueprint_main-showstyle'], + }, isHidden: false, }) @@ -230,7 +250,7 @@ describe('PropertiesPanel', () => { test('renders empty when no element selected', () => { const { container } = render(, { wrapper }) expect(container.querySelector('.properties-panel')).toBeTruthy() - expect(container.querySelector('.propertiespanel-pop-up__contents')).toBeFalsy() + expect(container.querySelector('.properties-panel-pop-up__form')).toBeFalsy() }) test('renders segment properties when segment is selected', async () => { @@ -308,8 +328,8 @@ describe('PropertiesPanel', () => { }) // Wait for the switch button to be available - const { container } = renderWithContext(, { ctxValue: result.current }) - const switchButton = await waitFor(() => container.querySelector('.propertiespanel-pop-up__switchbutton')) + renderWithContext(, { ctxValue: result.current }) + const switchButton = await waitFor(() => screen.getByText('TEST_LABEL')) expect(switchButton).toBeTruthy() if (!switchButton) return // above would have thrown - this is a type guard @@ -318,7 +338,7 @@ describe('PropertiesPanel', () => { await userEvent.click(switchButton) // Check if commit button is enabled - const commitButton = screen.getByText('COMMIT CHANGES') + const commitButton = screen.getByText('Save') expect(commitButton).toBeEnabled() // Commit changes @@ -366,7 +386,7 @@ describe('PropertiesPanel', () => { }) // Click revert button - const revertButton = screen.getByText('REVERT CHANGES') + const revertButton = screen.getByText('Restore Segment from NRCS') await act(async () => { await userEvent.click(revertButton) }) @@ -381,7 +401,7 @@ describe('PropertiesPanel', () => { pieceExternalId: undefined, }, { - id: 'revert-segment', + id: '__sofie-revert-segment', } ) }) @@ -407,6 +427,7 @@ describe('PropertiesPanel', () => { await userEvent.click(closeButton!) }) - expect(container.querySelector('.propertiespanel-pop-up__contents')).toBeFalsy() + // expect(container.querySelector('.propertiespanel-pop-up__contents')).toBeFalsy() + expect(container.querySelector('.properties-panel-pop-up__form')).toBeFalsy() }) }) From dedebeec8fda425528889ae5b723830e2737148f Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Thu, 12 Dec 2024 09:44:09 +0000 Subject: [PATCH 89/90] chore: remove unused parameter --- .../client/ui/UserEditOperations/RenderUserEditOperations.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx index 200db5d884..f33ab76b4e 100644 --- a/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/RenderUserEditOperations.tsx @@ -21,7 +21,6 @@ interface UserEditOperationMenuItemsProps { export function UserEditOperationMenuItems({ rundownId, - targetName, operationTarget, userEditOperations, isFormEditable, From 287f0079b2b6b9393a718eca7add1a24909b2700 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Fri, 13 Dec 2024 09:20:50 +0000 Subject: [PATCH 90/90] chore: removed commented styles --- .../webui/src/client/styles/propertiesPanel.scss | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 50495487b1..e8ecd5d9ce 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -47,8 +47,6 @@ background: #0a20ed; color: #ddd; - // min-width: 2.5rem; - // height: 2.5rem; max-width: 100%; display: flex; @@ -57,12 +55,6 @@ gap: 0.2em; align-self: stretch; - // text-align: left; - // padding-left: 1rem; - // display: flex; - // align-items: center; - // gap: 0.5rem; - text-shadow: 0.5px 0.5px 8px rgba(0, 0, 0, 0.8); font-family: Roboto; font-size: 1em; @@ -72,7 +64,6 @@ letter-spacing: 0.5px; > .svg { - // filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.15)); width: 1em; height: 1.2em; flex-shrink: 0; @@ -105,19 +96,12 @@ > .propertiespanel-pop-up__footer { flex: 1; - // position: absolute; - // left: 0; - // right: 0; - // bottom: 0; flex: 0 0 0; padding: 10px; display: flex; justify-content: space-between; - // align-items: center; gap: 12px; - // margin-right: 50px; - > .propertiespanel-pop-up__button-group { display: inherit; gap: inherit;