From 3f1294156530286aee83c5cfc37a7a63d5af6560 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sun, 15 Dec 2024 23:05:14 +0000 Subject: [PATCH 1/6] wip --- companion/lib/Internal/BuildingBlocks.ts | 15 +++++++++++++++ companion/lib/Internal/Controller.ts | 1 + companion/lib/Internal/Types.ts | 2 +- shared-lib/lib/Model/FeedbackDefinitionModel.ts | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/companion/lib/Internal/BuildingBlocks.ts b/companion/lib/Internal/BuildingBlocks.ts index b28ef3ebc..317a9c86f 100644 --- a/companion/lib/Internal/BuildingBlocks.ts +++ b/companion/lib/Internal/BuildingBlocks.ts @@ -85,6 +85,18 @@ export class InternalBuildingBlocks implements InternalModuleFragment { learnTimeout: undefined, supportsChildFeedbacks: true, }, + conditionalise_advanced: { + type: 'advanced', + label: 'Conditionalise existing feedbacks', + description: "Make 'advanced' feedbacks conditional", + style: undefined, + showInvert: false, + options: [], + hasLearn: false, + learnTimeout: undefined, + supportsChildFeedbacks: true, + supportsAdvancedChildFeedbacks: true, + }, } } @@ -144,6 +156,9 @@ export class InternalBuildingBlocks implements InternalModuleFragment { } else if (feedback.type === 'logic_xor') { const isSingleTrue = childValues.reduce((acc, val) => acc + (val ? 1 : 0), 0) === 1 return isSingleTrue === !feedback.isInverted + } else if (feedback.type === 'conditionalise_advanced') { + console.log('TODO', feedback) + return false } else { this.#logger.warn(`Unexpected logic feedback type "${feedback.type}"`) return false diff --git a/companion/lib/Internal/Controller.ts b/companion/lib/Internal/Controller.ts index 058a6a7a6..cc6ded988 100644 --- a/companion/lib/Internal/Controller.ts +++ b/companion/lib/Internal/Controller.ts @@ -438,6 +438,7 @@ export class InternalController { showButtonPreview: feedback.showButtonPreview ?? false, supportsChildFeedbacks: feedback.supportsChildFeedbacks ?? false, + supportsAdvancedChildFeedbacks: feedback.supportsAdvancedChildFeedbacks ?? false, } } } diff --git a/companion/lib/Internal/Types.ts b/companion/lib/Internal/Types.ts index 1f01286fa..dcade9a38 100644 --- a/companion/lib/Internal/Types.ts +++ b/companion/lib/Internal/Types.ts @@ -82,5 +82,5 @@ export type InternalActionDefinition = SetOptional< export type InternalFeedbackDefinition = SetOptional< FeedbackDefinition, - 'hasLearn' | 'learnTimeout' | 'showButtonPreview' | 'supportsChildFeedbacks' + 'hasLearn' | 'learnTimeout' | 'showButtonPreview' | 'supportsChildFeedbacks' | 'supportsAdvancedChildFeedbacks' > diff --git a/shared-lib/lib/Model/FeedbackDefinitionModel.ts b/shared-lib/lib/Model/FeedbackDefinitionModel.ts index 6cdb43b80..a31ea1004 100644 --- a/shared-lib/lib/Model/FeedbackDefinitionModel.ts +++ b/shared-lib/lib/Model/FeedbackDefinitionModel.ts @@ -14,6 +14,7 @@ export interface FeedbackDefinition { showButtonPreview: boolean supportsChildFeedbacks: boolean + supportsAdvancedChildFeedbacks: boolean } export type ClientFeedbackDefinition = FeedbackDefinition From cf4b750f78da253140a87392f2b4e0ea39af076f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Dec 2024 20:47:28 +0000 Subject: [PATCH 2/6] wip: expand feedback fragments to support multiple types of children --- companion/lib/Controls/Controller.ts | 11 +- .../Fragments/FragmentFeedbackInstance.ts | 150 ++++++++++++++---- .../Fragments/FragmentFeedbackList.ts | 28 ++-- .../Controls/Fragments/FragmentFeedbacks.ts | 36 +++-- shared-lib/lib/Model/FeedbackModel.ts | 8 + shared-lib/lib/SocketIO.ts | 5 +- .../Controls/ControlFeedbacksService.ts | 15 +- 7 files changed, 182 insertions(+), 71 deletions(-) diff --git a/companion/lib/Controls/Controller.ts b/companion/lib/Controls/Controller.ts index 5e02ae805..1b33d120b 100644 --- a/companion/lib/Controls/Controller.ts +++ b/companion/lib/Controls/Controller.ts @@ -323,7 +323,7 @@ export class ControlsController extends CoreBase { } }) - client.onPromise('controls:feedback:add', (controlId, parentId, connectionId, feedbackId) => { + client.onPromise('controls:feedback:add', (controlId, ownerId, connectionId, feedbackId) => { const control = this.getControl(controlId) if (!control) return false @@ -334,7 +334,7 @@ export class ControlsController extends CoreBase { control.feedbacks.isBooleanOnly ) if (feedbackItem) { - return control.feedbacks.feedbackAdd(feedbackItem, parentId) + return control.feedbacks.feedbackAdd(feedbackItem, ownerId) } else { return false } @@ -450,14 +450,15 @@ export class ControlsController extends CoreBase { } }) - client.onPromise('controls:feedback:move', (controlId, moveFeedbackId, newParentId, newIndex) => { + client.onPromise('controls:feedback:move', (controlId, moveFeedbackId, newOwnerId, newIndex) => { const control = this.getControl(controlId) if (!control) return false - if (moveFeedbackId === newParentId) throw new Error('Cannot move feedback to itself') + if (newOwnerId && moveFeedbackId === newOwnerId.parentFeedbackId) + throw new Error('Cannot move feedback to itself') if (control.supportsFeedbacks) { - return control.feedbacks.feedbackMoveTo(moveFeedbackId, newParentId, newIndex) + return control.feedbacks.feedbackMoveTo(moveFeedbackId, newOwnerId, newIndex) } else { throw new Error(`Control "${controlId}" does not support feedbacks`) } diff --git a/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts b/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts index 5f0ab92ef..5d1aa43e8 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts @@ -6,11 +6,12 @@ import { visitFeedbackInstance } from '../../Resources/Visitors/FeedbackInstance import type { InstanceDefinitions } from '../../Instance/Definitions.js' import type { InternalController } from '../../Internal/Controller.js' import type { ModuleHost } from '../../Instance/Host.js' -import type { FeedbackInstance } from '@companion-app/shared/Model/FeedbackModel.js' +import type { FeedbackChildGroup, FeedbackInstance } from '@companion-app/shared/Model/FeedbackModel.js' import type { ButtonStyleProperties } from '@companion-app/shared/Model/StyleModel.js' import type { CompanionButtonStyleProps } from '@companion-module/base' import type { InternalVisitor } from '../../Internal/Types.js' import type { FeedbackDefinition } from '@companion-app/shared/Model/FeedbackDefinitionModel.js' +import { assertNever } from '@companion-app/shared/Util.js' export class FragmentFeedbackInstance { /** @@ -29,7 +30,7 @@ export class FragmentFeedbackInstance { readonly #data: Omit - #children: FragmentFeedbackList + #children = new Map() /** * Value of the feedback when it was last executed @@ -96,17 +97,64 @@ export class FragmentFeedbackInstance { this.#data.id = nanoid() } - this.#children = new FragmentFeedbackList( + try { + const childGroup = this.#getOrCreateFeedbackGroup('children') + if (data.instance_id === 'internal' && data.children) { + childGroup.loadStorage(data.children, true, isCloned) + } + } catch (e: any) { + this.#logger.error(`Error loading child feedback group: ${e.message}`) + } + + try { + const childGroup = this.#getOrCreateFeedbackGroup('advancedChildren') + if (data.instance_id === 'internal' && data.advancedChildren) { + childGroup.loadStorage(data.advancedChildren, true, isCloned) + } + } catch (e: any) { + this.#logger.error(`Error loading advancedChildren feedback group: ${e.message}`) + } + } + + #getOrCreateFeedbackGroup(groupId: FeedbackChildGroup): FragmentFeedbackList { + const existing = this.#children.get(groupId) + if (existing) return existing + + // Check what names are allowed + const definition = this.connectionId === 'internal' && this.getDefinition() + if (!definition) throw new Error('Feedback cannot accept children.') + + let childType: 'boolean' | 'advanced' + + switch (groupId) { + case 'children': + childType = 'boolean' + if (!definition.supportsChildFeedbacks) { + throw new Error('Feedback cannot accept children in this group.') + } + break + case 'advancedChildren': + childType = 'advanced' + if (!definition.supportsAdvancedChildFeedbacks) { + throw new Error("Feedback cannot accept 'advanced' children in this group.") + } + break + default: + assertNever(groupId) + throw new Error(`Feedback cannot accept children of type "${groupId}".`) + } + + const childGroup = new FragmentFeedbackList( this.#instanceDefinitions, this.#internalModule, this.#moduleHost, this.#controlId, - this.id, - true + { parentFeedbackId: this.id, childGroup: groupId }, + childType ) - if (data.instance_id === 'internal' && data.children) { - this.#children.loadStorage(data.children, true, isCloned) - } + this.#children.set(groupId, childGroup) + + return childGroup } /** @@ -115,7 +163,9 @@ export class FragmentFeedbackInstance { asFeedbackInstance(): FeedbackInstance { return { ...this.#data, - children: this.connectionId === 'internal' ? this.#children.asFeedbackInstances() : undefined, + children: this.connectionId === 'internal' ? this.#children.get('children')?.asFeedbackInstances() : undefined, + advancedChildren: + this.connectionId === 'internal' ? this.#children.get('advancedChildren')?.asFeedbackInstances() : undefined, } } @@ -138,7 +188,7 @@ export class FragmentFeedbackInstance { // Special case to handle the internal 'logic' operators, which need to be executed live if (this.connectionId === 'internal' && this.#data.type.startsWith('logic_')) { // Future: This could probably be made a bit more generic by checking `definition.supportsChildFeedbacks` - const childValues = this.#children.getChildBooleanValues() + const childValues = this.#children.get('children')?.getChildBooleanValues() ?? [] return this.#internalModule.executeLogicFeedback(this.asFeedbackInstance(), childValues) } @@ -168,7 +218,9 @@ export class FragmentFeedbackInstance { // Remove from cached feedback values this.#cachedValue = undefined - this.#children.cleanup() + for (const childGroup of this.#children.values()) { + childGroup.cleanup() + } } /** @@ -193,7 +245,9 @@ export class FragmentFeedbackInstance { } if (recursive) { - this.#children.subscribe(recursive, onlyConnectionId) + for (const childGroup of this.#children.values()) { + childGroup.subscribe(recursive, onlyConnectionId) + } } } @@ -387,8 +441,10 @@ export class FragmentFeedbackInstance { changed = true } - if (this.#children.clearCachedValueForConnectionId(connectionId)) { - changed = true + for (const childGroup of this.#children.values()) { + if (childGroup.clearCachedValueForConnectionId(connectionId)) { + changed = true + } } return changed @@ -398,7 +454,11 @@ export class FragmentFeedbackInstance { * Find a child feedback by id */ findChildById(id: string): FragmentFeedbackInstance | undefined { - return this.#children.findById(id) + for (const childGroup of this.#children.values()) { + const result = childGroup.findById(id) + if (result) return result + } + return undefined } /** @@ -407,32 +467,44 @@ export class FragmentFeedbackInstance { findParentAndIndex( id: string ): { parent: FragmentFeedbackList; index: number; item: FragmentFeedbackInstance } | undefined { - return this.#children.findParentAndIndex(id) + for (const childGroup of this.#children.values()) { + const result = childGroup.findParentAndIndex(id) + if (result) return result + } + return undefined } /** * Add a child feedback to this feedback */ - addChild(feedback: FeedbackInstance): FragmentFeedbackInstance { + addChild(groupId: FeedbackChildGroup, feedback: FeedbackInstance): FragmentFeedbackInstance { if (this.connectionId !== 'internal') { throw new Error('Only internal feedbacks can have children') } - return this.#children.addFeedback(feedback) + const childGroup = this.#getOrCreateFeedbackGroup(groupId) + return childGroup.addFeedback(feedback) } /** * Remove a child feedback */ removeChild(id: string): boolean { - return this.#children.removeFeedback(id) + for (const childGroup of this.#children.values()) { + if (childGroup.removeFeedback(id)) return true + } + return false } /** * Duplicate a child feedback */ duplicateChild(id: string): FragmentFeedbackInstance | undefined { - return this.#children.duplicateFeedback(id) + for (const childGroup of this.#children.values()) { + const newFeedback = childGroup.duplicateFeedback(id) + if (newFeedback) return newFeedback + } + return undefined } // /** @@ -454,29 +526,43 @@ export class FragmentFeedbackInstance { * Push a child feedback to the list * Note: this is used when moving a feedback from a different parent. Lifecycle is not managed */ - pushChild(feedback: FragmentFeedbackInstance, index: number): void { - return this.#children.pushFeedback(feedback, index) + pushChild(feedback: FragmentFeedbackInstance, groupId: FeedbackChildGroup, index: number): void { + const childGroup = this.#getOrCreateFeedbackGroup(groupId) + return childGroup.pushFeedback(feedback, index) } /** * Check if this list can accept a specified child */ - canAcceptChild(feedback: FragmentFeedbackInstance): boolean { - return this.#children.canAcceptFeedback(feedback) + canAcceptChild(groupId: FeedbackChildGroup, feedback: FragmentFeedbackInstance): boolean { + const childGroup = this.#getOrCreateFeedbackGroup(groupId) + return childGroup.canAcceptFeedback(feedback) } /** * Recursively get all the feedbacks */ getAllChildren(): FragmentFeedbackInstance[] { - return this.#children.getAllFeedbacks() + const feedbacks: FragmentFeedbackInstance[] = [] + + for (const childGroup of this.#children.values()) { + feedbacks.push(...childGroup.getAllFeedbacks()) + } + + return feedbacks } /** * Cleanup and forget any children belonging to the given connection */ forgetChildrenForConnection(connectionId: string): boolean { - return this.#children.forgetForConnection(connectionId) + let changed = false + for (const childGroup of this.#children.values()) { + if (childGroup.forgetForConnection(connectionId)) { + changed = true + } + } + return changed } /** @@ -484,7 +570,13 @@ export class FragmentFeedbackInstance { * Doesn't do any cleanup, as it is assumed that the connection has not been running */ verifyChildConnectionIds(knownConnectionIds: Set): boolean { - return this.#children.verifyConnectionIds(knownConnectionIds) + let changed = false + for (const childGroup of this.#children.values()) { + if (childGroup.verifyConnectionIds(knownConnectionIds)) { + changed = true + } + } + return changed } /** @@ -509,7 +601,9 @@ export class FragmentFeedbackInstance { } } - ps.push(...this.#children.postProcessImport()) + for (const childGroup of this.#children.values()) { + ps.push(...childGroup.postProcessImport()) + } return ps } diff --git a/companion/lib/Controls/Fragments/FragmentFeedbackList.ts b/companion/lib/Controls/Fragments/FragmentFeedbackList.ts index 05fef5ed8..e76449eae 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbackList.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbackList.ts @@ -3,7 +3,7 @@ import { clamp } from '../../Resources/Util.js' import type { InstanceDefinitions } from '../../Instance/Definitions.js' import type { InternalController } from '../../Internal/Controller.js' import type { ModuleHost } from '../../Instance/Host.js' -import type { FeedbackInstance } from '@companion-app/shared/Model/FeedbackModel.js' +import type { FeedbackInstance, FeedbackOwner } from '@companion-app/shared/Model/FeedbackModel.js' import type { ButtonStyleProperties, UnparsedButtonStyle } from '@companion-app/shared/Model/StyleModel.js' export class FragmentFeedbackList { @@ -16,17 +16,17 @@ export class FragmentFeedbackList { */ readonly #controlId: string - readonly #id: string | null + readonly #ownerId: FeedbackOwner | null /** * Whether this set of feedbacks can only use boolean feedbacks */ - readonly #booleanOnly: boolean + readonly #onlyType: 'boolean' | 'advanced' | null #feedbacks: FragmentFeedbackInstance[] = [] - get id(): string | null { - return this.#id + get ownerId(): FeedbackOwner | null { + return this.#ownerId } constructor( @@ -34,15 +34,15 @@ export class FragmentFeedbackList { internalModule: InternalController, moduleHost: ModuleHost, controlId: string, - id: string | null, - booleanOnly: boolean + ownerId: FeedbackOwner | null, + onlyType: 'boolean' | 'advanced' | null ) { this.#instanceDefinitions = instanceDefinitions this.#internalModule = internalModule this.#moduleHost = moduleHost this.#controlId = controlId - this.#id = id - this.#booleanOnly = booleanOnly + this.#ownerId = ownerId + this.#onlyType = onlyType } /** @@ -63,7 +63,7 @@ export class FragmentFeedbackList { * Get the value of this feedback as a boolean */ getBooleanValue(): boolean { - if (!this.#booleanOnly) throw new Error('FragmentFeedbacks is setup to use styles') + if (this.#onlyType !== 'boolean') throw new Error('FragmentFeedbacks is setup to use styles') let result = true @@ -77,7 +77,7 @@ export class FragmentFeedbackList { } getChildBooleanValues(): boolean[] { - if (!this.#booleanOnly) throw new Error('FragmentFeedbacks is setup to use styles') + if (this.#onlyType !== 'boolean') throw new Error('FragmentFeedbacks is setup to use styles') const values: boolean[] = [] @@ -270,10 +270,10 @@ export class FragmentFeedbackList { * Check if this list can accept a specified child */ canAcceptFeedback(feedback: FragmentFeedbackInstance): boolean { - if (!this.#booleanOnly) return true + if (!this.#onlyType) return true const definition = feedback.getDefinition() - if (!definition || definition.type !== 'boolean') return false + if (!definition || definition.type !== this.#onlyType) return false return true } @@ -355,7 +355,7 @@ export class FragmentFeedbackList { * @returns the unprocessed style */ getUnparsedStyle(baseStyle: ButtonStyleProperties): UnparsedButtonStyle { - if (this.#booleanOnly) throw new Error('FragmentFeedbacks not setup to use styles') + if (this.#onlyType == 'boolean') throw new Error('FragmentFeedbacks not setup to use styles') let style: UnparsedButtonStyle = { ...baseStyle, diff --git a/companion/lib/Controls/Fragments/FragmentFeedbacks.ts b/companion/lib/Controls/Fragments/FragmentFeedbacks.ts index d166b8d71..7ca541c11 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbacks.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbacks.ts @@ -6,7 +6,7 @@ import type { ButtonStyleProperties, UnparsedButtonStyle } from '@companion-app/ import type { InstanceDefinitions } from '../../Instance/Definitions.js' import type { InternalController } from '../../Internal/Controller.js' import type { ModuleHost } from '../../Instance/Host.js' -import { FeedbackInstance } from '@companion-app/shared/Model/FeedbackModel.js' +import type { FeedbackInstance, FeedbackOwner } from '@companion-app/shared/Model/FeedbackModel.js' /** * Helper for ControlTypes with feedbacks @@ -105,7 +105,7 @@ export class FragmentFeedbacks { moduleHost, this.#controlId, null, - this.#booleanOnly + this.#booleanOnly ? 'boolean' : null ) } @@ -145,16 +145,17 @@ export class FragmentFeedbacks { /** * Add a feedback to this control * @param feedbackItem the item to add - * @param parentId the ids of parent feedback that this feedback should be added as a child of + * @param ownerId the ids of parent feedback that this feedback should be added as a child of */ - feedbackAdd(feedbackItem: FeedbackInstance, parentId: string | null): boolean { + feedbackAdd(feedbackItem: FeedbackInstance, ownerId: FeedbackOwner | null): boolean { let newFeedback: FragmentFeedbackInstance - if (parentId) { - const parent = this.#feedbacks.findById(parentId) - if (!parent) throw new Error(`Failed to find parent feedback ${parentId} when adding child feedback`) + if (ownerId) { + const parent = this.#feedbacks.findById(ownerId.parentFeedbackId) + if (!parent) + throw new Error(`Failed to find parent feedback ${ownerId.parentFeedbackId} when adding child feedback`) - newFeedback = parent.addChild(feedbackItem) + newFeedback = parent.addChild(ownerId.childGroup, feedbackItem) } else { newFeedback = this.#feedbacks.addFeedback(feedbackItem) } @@ -248,30 +249,33 @@ export class FragmentFeedbacks { /** * Move a feedback within the hierarchy * @param moveFeedbackId the id of the feedback to move - * @param newParentId the target parentId of the feedback + * @param newOwnerId the target parentId of the feedback * @param newIndex the target index of the feedback */ - feedbackMoveTo(moveFeedbackId: string, newParentId: string | null, newIndex: number): boolean { + feedbackMoveTo(moveFeedbackId: string, newOwnerId: FeedbackOwner | null, newIndex: number): boolean { const oldItem = this.#feedbacks.findParentAndIndex(moveFeedbackId) if (!oldItem) return false - if (oldItem.parent.id === newParentId) { + if ( + oldItem.parent.ownerId?.parentFeedbackId === newOwnerId?.parentFeedbackId && + oldItem.parent.ownerId?.childGroup === newOwnerId?.childGroup + ) { oldItem.parent.moveFeedback(oldItem.index, newIndex) } else { - const newParent = newParentId ? this.#feedbacks.findById(newParentId) : null - if (newParentId && !newParent) return false + const newParent = newOwnerId ? this.#feedbacks.findById(newOwnerId.parentFeedbackId) : null + if (newOwnerId && !newParent) return false // Ensure the new parent is not a child of the feedback being moved - if (newParentId && oldItem.item.findChildById(newParentId)) return false + if (newOwnerId && oldItem.item.findChildById(newOwnerId.parentFeedbackId)) return false // Check if the new parent can hold the feedback being moved - if (newParent && !newParent.canAcceptChild(oldItem.item)) return false + if (newParent && !newParent.canAcceptChild(newOwnerId!.childGroup, oldItem.item)) return false const poppedFeedback = oldItem.parent.popFeedback(oldItem.index) if (!poppedFeedback) return false if (newParent) { - newParent.pushChild(poppedFeedback, newIndex) + newParent.pushChild(poppedFeedback, newOwnerId!.childGroup, newIndex) } else { this.#feedbacks.pushFeedback(poppedFeedback, newIndex) } diff --git a/shared-lib/lib/Model/FeedbackModel.ts b/shared-lib/lib/Model/FeedbackModel.ts index b7db7a549..138ef635e 100644 --- a/shared-lib/lib/Model/FeedbackModel.ts +++ b/shared-lib/lib/Model/FeedbackModel.ts @@ -12,4 +12,12 @@ export interface FeedbackInstance { style?: Partial children?: FeedbackInstance[] + advancedChildren?: FeedbackInstance[] +} + +export type FeedbackChildGroup = 'children' | 'advancedChildren' + +export interface FeedbackOwner { + parentFeedbackId: string + childGroup: FeedbackChildGroup } diff --git a/shared-lib/lib/SocketIO.ts b/shared-lib/lib/SocketIO.ts index 057a674d0..45fd64e3b 100644 --- a/shared-lib/lib/SocketIO.ts +++ b/shared-lib/lib/SocketIO.ts @@ -45,6 +45,7 @@ import type { CloudControllerState, CloudRegionState } from './Model/Cloud.js' import type { ModuleInfoUpdate, ModuleDisplayInfo } from './Model/ModuleInfo.js' import type { ClientConnectionsUpdate, ClientConnectionConfig } from './Model/Connections.js' import type { ActionOwner, ActionSetId } from './Model/ActionModel.js' +import type { FeedbackOwner } from './Model/FeedbackModel.js' export interface ClientToBackendEventsMap { disconnect: () => never // Hack because type is missing @@ -121,12 +122,12 @@ export interface ClientToBackendEventsMap { 'controls:feedback:move': ( controlId: string, dragFeedbackId: string, - hoverParentId: string | null, + hoverOwnerId: FeedbackOwner | null, hoverIndex: number ) => boolean 'controls:feedback:add': ( controlId: string, - parentId: string | null, + ownerId: FeedbackOwner | null, connectionId: string, feedbackType: string ) => boolean diff --git a/webui/src/Services/Controls/ControlFeedbacksService.ts b/webui/src/Services/Controls/ControlFeedbacksService.ts index 87a9e3ad7..193273f60 100644 --- a/webui/src/Services/Controls/ControlFeedbacksService.ts +++ b/webui/src/Services/Controls/ControlFeedbacksService.ts @@ -43,17 +43,20 @@ export function useControlFeedbacksEditorService( () => ({ addFeedback: (feedbackType: string, parentId: string | null) => { const [connectionId, feedbackId] = feedbackType.split(':', 2) - socketEmitPromise(socket, 'controls:feedback:add', [controlId, parentId, connectionId, feedbackId]).catch( - (e) => { - console.error('Failed to add control feedback', e) - } - ) + socketEmitPromise(socket, 'controls:feedback:add', [ + controlId, + parentId ? { parentFeedbackId: parentId, childGroup: 'children' } : null, + connectionId, + feedbackId, + ]).catch((e) => { + console.error('Failed to add control feedback', e) + }) }, moveCard: (dragFeedbackId: string, hoverParentId: string | null, hoverIndex: number) => { socketEmitPromise(socket, 'controls:feedback:move', [ controlId, dragFeedbackId, - hoverParentId, + hoverParentId ? { parentFeedbackId: hoverParentId, childGroup: 'children' } : null, hoverIndex, ]).catch((e) => { console.error(`Move failed: ${e}`) From ab2a59a38443f455b1d5b6bd15e2a31bab72666b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Dec 2024 21:52:06 +0000 Subject: [PATCH 3/6] wip: 'Conditionalise existing feedbacks' feedback --- .../Fragments/FeedbackStyleBuilder.ts | 58 ++++++++ .../Fragments/FragmentFeedbackInstance.ts | 14 +- .../Fragments/FragmentFeedbackList.ts | 62 +++------ .../Controls/Fragments/FragmentFeedbacks.ts | 5 +- companion/lib/Internal/BuildingBlocks.ts | 7 +- webui/src/Buttons/EditButton.tsx | 2 +- webui/src/Controls/AddFeedbackDropdown.tsx | 36 ++--- webui/src/Controls/AddModal.tsx | 16 ++- webui/src/Controls/FeedbackEditor.tsx | 130 +++++++++++------- .../Controls/ControlFeedbacksService.ts | 25 ++-- webui/src/Triggers/EditPanel.tsx | 2 +- 11 files changed, 219 insertions(+), 138 deletions(-) create mode 100644 companion/lib/Controls/Fragments/FeedbackStyleBuilder.ts diff --git a/companion/lib/Controls/Fragments/FeedbackStyleBuilder.ts b/companion/lib/Controls/Fragments/FeedbackStyleBuilder.ts new file mode 100644 index 000000000..bec1a82ab --- /dev/null +++ b/companion/lib/Controls/Fragments/FeedbackStyleBuilder.ts @@ -0,0 +1,58 @@ +import type { ButtonStyleProperties, UnparsedButtonStyle } from '@companion-app/shared/Model/StyleModel.js' + +/** + * A simple class to help combine multiple styles into one that can be drawn + */ +export class FeedbackStyleBuilder { + #combinedStyle: UnparsedButtonStyle + + get style(): UnparsedButtonStyle { + return this.#combinedStyle + } + + constructor(baseStyle: ButtonStyleProperties) { + this.#combinedStyle = { + ...baseStyle, + imageBuffers: [], + } + } + + /** + * Apply a simple layer of style + */ + applySimpleStyle(style: Partial | undefined) { + this.#combinedStyle = { + ...this.#combinedStyle, + ...style, + } + } + + applyComplexStyle(rawValue: any) { + if (typeof rawValue === 'object' && rawValue !== undefined) { + // Prune off some special properties + const prunedValue = { ...rawValue } + delete prunedValue.imageBuffer + delete prunedValue.imageBufferPosition + + // Ensure `textExpression` is set at the same times as `text` + delete prunedValue.textExpression + if ('text' in prunedValue) { + prunedValue.textExpression = rawValue.textExpression || false + } + + this.#combinedStyle = { + ...this.#combinedStyle, + ...prunedValue, + } + + // Push the imageBuffer into an array + if (rawValue.imageBuffer) { + this.#combinedStyle.imageBuffers.push({ + ...rawValue.imageBufferPosition, + ...rawValue.imageBufferEncoding, + buffer: rawValue.imageBuffer, + }) + } + } + } +} diff --git a/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts b/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts index 5d1aa43e8..ab5433920 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts @@ -48,6 +48,10 @@ export class FragmentFeedbackInstance { return !!this.#data.disabled } + get feedbackId(): string { + return this.#data.type + } + /** * Get the id of the connection this feedback belongs to */ @@ -183,7 +187,6 @@ export class FragmentFeedbackInstance { if (this.#data.disabled) return false const definition = this.getDefinition() - if (!definition || definition.type !== 'boolean') return false // Special case to handle the internal 'logic' operators, which need to be executed live if (this.connectionId === 'internal' && this.#data.type.startsWith('logic_')) { @@ -193,6 +196,8 @@ export class FragmentFeedbackInstance { return this.#internalModule.executeLogicFeedback(this.asFeedbackInstance(), childValues) } + if (!definition || definition.type !== 'boolean') return false + if (typeof this.#cachedValue === 'boolean') { if (definition.showInvert && this.#data.isInverted) return !this.#cachedValue @@ -552,6 +557,13 @@ export class FragmentFeedbackInstance { return feedbacks } + /** + * Recursively get all the feedbacks + */ + getChildrenOfGroup(groupId: FeedbackChildGroup): FragmentFeedbackInstance[] { + return this.#children.get(groupId)?.getFeedbacks() ?? [] + } + /** * Cleanup and forget any children belonging to the given connection */ diff --git a/companion/lib/Controls/Fragments/FragmentFeedbackList.ts b/companion/lib/Controls/Fragments/FragmentFeedbackList.ts index e76449eae..b021ede61 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbackList.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbackList.ts @@ -4,7 +4,7 @@ import type { InstanceDefinitions } from '../../Instance/Definitions.js' import type { InternalController } from '../../Internal/Controller.js' import type { ModuleHost } from '../../Instance/Host.js' import type { FeedbackInstance, FeedbackOwner } from '@companion-app/shared/Model/FeedbackModel.js' -import type { ButtonStyleProperties, UnparsedButtonStyle } from '@companion-app/shared/Model/StyleModel.js' +import type { FeedbackStyleBuilder } from './FeedbackStyleBuilder.js' export class FragmentFeedbackList { readonly #instanceDefinitions: InstanceDefinitions @@ -45,6 +45,13 @@ export class FragmentFeedbackList { this.#onlyType = onlyType } + /** + * Get the feedbacks + */ + getFeedbacks(): FragmentFeedbackInstance[] { + return this.#feedbacks + } + /** * Get all the feedbacks */ @@ -206,7 +213,9 @@ export class FragmentFeedbackList { !!isCloned ) - // TODO - verify that the feedback matches this.#booleanOnly? + const feedbackDefinition = newFeedback.getDefinition() + if (this.#onlyType && feedbackDefinition?.type !== this.#onlyType) + throw new Error('FragmentFeedbacks cannot accept this type of feedback') this.#feedbacks.push(newFeedback) @@ -354,14 +363,9 @@ export class FragmentFeedbackList { * @param baseStyle Style of the button without feedbacks applied * @returns the unprocessed style */ - getUnparsedStyle(baseStyle: ButtonStyleProperties): UnparsedButtonStyle { + buildStyle(styleBuilder: FeedbackStyleBuilder): void { if (this.#onlyType == 'boolean') throw new Error('FragmentFeedbacks not setup to use styles') - let style: UnparsedButtonStyle = { - ...baseStyle, - imageBuffers: [], - } - // Note: We don't need to consider children of the feedbacks here, as that can only be from boolean feedbacks which are handled by the `getBooleanValue` for (const feedback of this.#feedbacks) { @@ -369,45 +373,19 @@ export class FragmentFeedbackList { const definition = feedback.getDefinition() if (definition?.type === 'boolean') { - const booleanValue = feedback.getBooleanValue() - if (booleanValue) { - style = { - ...style, - ...feedback.asFeedbackInstance().style, - } - } + if (feedback.getBooleanValue()) styleBuilder.applySimpleStyle(feedback.asFeedbackInstance().style) } else if (definition?.type === 'advanced') { - const rawValue = feedback.cachedValue - if (typeof rawValue === 'object' && rawValue !== undefined) { - // Prune off some special properties - const prunedValue = { ...rawValue } - delete prunedValue.imageBuffer - delete prunedValue.imageBufferPosition - - // Ensure `textExpression` is set at the same times as `text` - delete prunedValue.textExpression - if ('text' in prunedValue) { - prunedValue.textExpression = rawValue.textExpression || false - } - - style = { - ...style, - ...prunedValue, - } - - // Push the imageBuffer into an array - if (rawValue.imageBuffer) { - style.imageBuffers.push({ - ...rawValue.imageBufferPosition, - ...rawValue.imageBufferEncoding, - buffer: rawValue.imageBuffer, - }) + if (feedback.connectionId === 'internal' && feedback.feedbackId === 'logic_conditionalise_advanced') { + if (feedback.getBooleanValue()) { + for (const child of feedback.getChildrenOfGroup('advancedChildren')) { + styleBuilder.applyComplexStyle(child.cachedValue) + } } + } else { + styleBuilder.applyComplexStyle(feedback.cachedValue) } } } - - return style } /** diff --git a/companion/lib/Controls/Fragments/FragmentFeedbacks.ts b/companion/lib/Controls/Fragments/FragmentFeedbacks.ts index 7ca541c11..cb92b200c 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbacks.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbacks.ts @@ -7,6 +7,7 @@ import type { InstanceDefinitions } from '../../Instance/Definitions.js' import type { InternalController } from '../../Internal/Controller.js' import type { ModuleHost } from '../../Instance/Host.js' import type { FeedbackInstance, FeedbackOwner } from '@companion-app/shared/Model/FeedbackModel.js' +import { FeedbackStyleBuilder } from './FeedbackStyleBuilder.js' /** * Helper for ControlTypes with feedbacks @@ -451,7 +452,9 @@ export class FragmentFeedbacks { * Note: Does not clone the style */ getUnparsedStyle(): UnparsedButtonStyle { - return this.#feedbacks.getUnparsedStyle(this.baseStyle) + const styleBuilder = new FeedbackStyleBuilder(this.baseStyle) + this.#feedbacks.buildStyle(styleBuilder) + return styleBuilder.style } /** diff --git a/companion/lib/Internal/BuildingBlocks.ts b/companion/lib/Internal/BuildingBlocks.ts index 317a9c86f..012033533 100644 --- a/companion/lib/Internal/BuildingBlocks.ts +++ b/companion/lib/Internal/BuildingBlocks.ts @@ -85,7 +85,7 @@ export class InternalBuildingBlocks implements InternalModuleFragment { learnTimeout: undefined, supportsChildFeedbacks: true, }, - conditionalise_advanced: { + logic_conditionalise_advanced: { type: 'advanced', label: 'Conditionalise existing feedbacks', description: "Make 'advanced' feedbacks conditional", @@ -147,7 +147,7 @@ export class InternalBuildingBlocks implements InternalModuleFragment { * Execute a logic feedback */ executeLogicFeedback(feedback: FeedbackInstance, childValues: boolean[]): boolean { - if (feedback.type === 'logic_and') { + if (feedback.type === 'logic_and' || feedback.type === 'logic_conditionalise_advanced') { if (childValues.length === 0) return !!feedback.isInverted return childValues.reduce((acc, val) => acc && val, true) === !feedback.isInverted @@ -156,9 +156,6 @@ export class InternalBuildingBlocks implements InternalModuleFragment { } else if (feedback.type === 'logic_xor') { const isSingleTrue = childValues.reduce((acc, val) => acc + (val ? 1 : 0), 0) === 1 return isSingleTrue === !feedback.isInverted - } else if (feedback.type === 'conditionalise_advanced') { - console.log('TODO', feedback) - return false } else { this.#logger.warn(`Unexpected logic feedback type "${feedback.type}"`) return false diff --git a/webui/src/Buttons/EditButton.tsx b/webui/src/Buttons/EditButton.tsx index fc2e9b2a6..4eedf201a 100644 --- a/webui/src/Buttons/EditButton.tsx +++ b/webui/src/Buttons/EditButton.tsx @@ -559,7 +559,7 @@ function TabsSection({ style, controlId, location, steps, runtimeProps, rotaryAc controlId={controlId} feedbacks={feedbacks} location={location} - booleanOnly={false} + onlyType={null} addPlaceholder="+ Add feedback" /> diff --git a/webui/src/Controls/AddFeedbackDropdown.tsx b/webui/src/Controls/AddFeedbackDropdown.tsx index 45ef021de..eb526c118 100644 --- a/webui/src/Controls/AddFeedbackDropdown.tsx +++ b/webui/src/Controls/AddFeedbackDropdown.tsx @@ -34,12 +34,12 @@ interface AddFeedbackGroup { } interface AddFeedbackDropdownProps { onSelect: (feedbackType: string) => void - booleanOnly: boolean + onlyType: 'boolean' | 'advanced' | null addPlaceholder: string } export const AddFeedbackDropdown = observer(function AddFeedbackDropdown({ onSelect, - booleanOnly, + onlyType, addPlaceholder, }: AddFeedbackDropdownProps) { const { connections, feedbackDefinitions, recentlyAddedFeedbacks } = useContext(RootAppStoreContext) @@ -49,7 +49,7 @@ export const AddFeedbackDropdown = observer(function AddFeedbackDropdown({ const options: Array = [] for (const [connectionId, instanceFeedbacks] of feedbackDefinitions.connections.entries()) { for (const [feedbackId, feedback] of instanceFeedbacks.entries()) { - if (!booleanOnly || feedback.type === 'boolean') { + if (!onlyType || feedback.type === onlyType) { const connectionLabel = connections.getLabel(connectionId) ?? connectionId const optionLabel = `${connectionLabel}: ${feedback.label}` options.push({ @@ -64,19 +64,21 @@ export const AddFeedbackDropdown = observer(function AddFeedbackDropdown({ const recents: AddFeedbackOption[] = [] for (const feedbackType of recentlyAddedFeedbacks.recentIds) { - if (feedbackType) { - const [connectionId, feedbackId] = feedbackType.split(':', 2) - const feedbackInfo = feedbackDefinitions.connections.get(connectionId)?.get(feedbackId) - if (feedbackInfo) { - const connectionLabel = connections.getLabel(connectionId) ?? connectionId - const optionLabel = `${connectionLabel}: ${feedbackInfo.label}` - recents.push({ - isRecent: true, - value: `${connectionId}:${feedbackId}`, - label: optionLabel, - fuzzy: fuzzyPrepare(optionLabel), - }) - } + if (!feedbackType) continue + + const [connectionId, feedbackId] = feedbackType.split(':', 2) + const feedbackInfo = feedbackDefinitions.connections.get(connectionId)?.get(feedbackId) + if (!feedbackInfo) continue + + if (!onlyType || feedbackInfo.type === onlyType) { + const connectionLabel = connections.getLabel(connectionId) ?? connectionId + const optionLabel = `${connectionLabel}: ${feedbackInfo.label}` + recents.push({ + isRecent: true, + value: `${connectionId}:${feedbackId}`, + label: optionLabel, + fuzzy: fuzzyPrepare(optionLabel), + }) } } @@ -86,7 +88,7 @@ export const AddFeedbackDropdown = observer(function AddFeedbackDropdown({ }) return options - }, [feedbackDefinitions, connections, booleanOnly, recentlyAddedFeedbacks.recentIds]) + }, [feedbackDefinitions, connections, onlyType, recentlyAddedFeedbacks.recentIds]) const innerChange = useCallback( (e: AddFeedbackOption | null) => { diff --git a/webui/src/Controls/AddModal.tsx b/webui/src/Controls/AddModal.tsx index 4e15b834a..b97ef25d2 100644 --- a/webui/src/Controls/AddModal.tsx +++ b/webui/src/Controls/AddModal.tsx @@ -80,6 +80,7 @@ export const AddActionsModal = observer( key={connectionId} connectionId={connectionId} items={actions} + onlyType={null} itemName="actions" expanded={!!filter || expanded[connectionId]} filter={filter} @@ -100,7 +101,7 @@ export const AddActionsModal = observer( interface AddFeedbacksModalProps { addFeedback: (feedbackType: string) => void - booleanOnly: boolean + onlyType: 'boolean' | 'advanced' | null entityType: string } export interface AddFeedbacksModalRef { @@ -109,7 +110,7 @@ export interface AddFeedbacksModalRef { export const AddFeedbacksModal = observer( forwardRef(function AddFeedbacksModal( - { addFeedback, booleanOnly, entityType }, + { addFeedback, onlyType, entityType }, ref ) { const { feedbackDefinitions, recentlyAddedFeedbacks } = useContext(RootAppStoreContext) @@ -175,7 +176,7 @@ export const AddFeedbacksModal = observer( itemName="feedbacks" expanded={!!filter || expanded[connectionId]} filter={filter} - booleanOnly={booleanOnly} + onlyType={onlyType} doToggle={toggleExpanded} doAdd={addFeedback2} /> @@ -205,7 +206,7 @@ interface ConnectionCollapseProps { itemName: string expanded: boolean filter: string - booleanOnly?: boolean + onlyType: string | null doToggle: (connectionId: string) => void doAdd: (itemId: string) => void } @@ -216,7 +217,7 @@ const ConnectionCollapse = observer(function ConnectionCollapse) { @@ -231,7 +232,8 @@ const ConnectionCollapse = observer(function ConnectionCollapse { - if (!info || !info.label || (booleanOnly && (!('type' in info) || info.type !== 'boolean'))) return null + if (!info || !info.label) return null + if (onlyType && (!('type' in info) || info.type !== onlyType)) return null return { fullId: `${connectionId}:${id}`, @@ -240,7 +242,7 @@ const ConnectionCollapse = observer(function ConnectionCollapse !!v) - }, [items, booleanOnly]) + }, [items, onlyType]) const searchResults = filter ? fuzzySearch(filter, allValues, { diff --git a/webui/src/Controls/FeedbackEditor.tsx b/webui/src/Controls/FeedbackEditor.tsx index 8d7653cbb..3acafb380 100644 --- a/webui/src/Controls/FeedbackEditor.tsx +++ b/webui/src/Controls/FeedbackEditor.tsx @@ -20,7 +20,7 @@ import { AddFeedbacksModal, AddFeedbacksModalRef } from './AddModal.js' import { PanelCollapseHelper, usePanelCollapseHelper } from '../Helpers/CollapseHelper.js' import { OptionButtonPreview } from './OptionButtonPreview.js' import { ButtonStyleProperties } from '@companion-app/shared/Style.js' -import { FeedbackInstance } from '@companion-app/shared/Model/FeedbackModel.js' +import { FeedbackInstance, FeedbackOwner } from '@companion-app/shared/Model/FeedbackModel.js' import { ClientFeedbackDefinition } from '@companion-app/shared/Model/FeedbackDefinitionModel.js' import { DropdownChoiceId } from '@companion-module/base' import { ControlLocation } from '@companion-app/shared/Model/Common.js' @@ -36,12 +36,13 @@ import { observer } from 'mobx-react-lite' import { RootAppStoreContext } from '../Stores/RootAppStore.js' import classNames from 'classnames' import { InlineHelp } from '../Components/InlineHelp.js' +import { isEqual } from 'lodash-es' interface ControlFeedbacksEditorProps { controlId: string feedbacks: FeedbackInstance[] heading: JSX.Element | string entityType: string - booleanOnly: boolean + onlyType: 'boolean' | 'advanced' | null location: ControlLocation | undefined addPlaceholder: string } @@ -53,6 +54,9 @@ function findAllFeedbackIdsDeep(feedbacks: FeedbackInstance[]): string[] { if (feedback.children) { result.push(...findAllFeedbackIdsDeep(feedback.children)) } + if (feedback.advancedChildren) { + result.push(...findAllFeedbackIdsDeep(feedback.advancedChildren)) + } } return result @@ -63,7 +67,7 @@ export function ControlFeedbacksEditor({ feedbacks, heading, entityType, - booleanOnly, + onlyType, location, addPlaceholder, }: ControlFeedbacksEditorProps) { @@ -84,11 +88,11 @@ export function ControlFeedbacksEditor({ heading={heading} feedbacks={feedbacks} entityType={entityType} - booleanOnly={booleanOnly} + onlyType={onlyType} location={location} addPlaceholder={addPlaceholder} feedbacksService={feedbacksService} - parentId={null} + ownerId={null} panelCollapseHelper={panelCollapseHelper} /> @@ -100,11 +104,11 @@ interface InlineFeedbacksEditorProps { heading: JSX.Element | string | null feedbacks: FeedbackInstance[] entityType: string - booleanOnly: boolean + onlyType: 'boolean' | 'advanced' | null location: ControlLocation | undefined addPlaceholder: string feedbacksService: IFeedbackEditorService - parentId: string | null + ownerId: FeedbackOwner | null panelCollapseHelper: PanelCollapseHelper } @@ -113,30 +117,32 @@ const InlineFeedbacksEditor = observer(function InlineFeedbacksEditor({ heading, feedbacks, entityType, - booleanOnly, + onlyType, location, addPlaceholder, feedbacksService, - parentId, + ownerId, panelCollapseHelper, }: InlineFeedbacksEditorProps) { const addFeedbacksRef = useRef(null) const showAddModal = useCallback(() => addFeedbacksRef.current?.show(), []) const addFeedback = useCallback( - (feedbackType: string) => feedbacksService.addFeedback(feedbackType, parentId), - [feedbacksService, parentId] + (feedbackType: string) => feedbacksService.addFeedback(feedbackType, ownerId), + [feedbacksService, ownerId] ) const childFeedbackIds = feedbacks.map((f) => f.id) + const expandGroupId = feedbackOwnerString(ownerId) + return ( <> @@ -147,19 +153,19 @@ const InlineFeedbacksEditor = observer(function InlineFeedbacksEditor({ {feedbacks.length >= 1 && ( - {panelCollapseHelper.canExpandAll(parentId, childFeedbackIds) && ( + {panelCollapseHelper.canExpandAll(expandGroupId, childFeedbackIds) && ( panelCollapseHelper.setAllExpanded(parentId, childFeedbackIds)} + onClick={() => panelCollapseHelper.setAllExpanded(expandGroupId, childFeedbackIds)} title="Expand all feedbacks" > )} - {panelCollapseHelper.canCollapseAll(parentId, childFeedbackIds) && ( + {panelCollapseHelper.canCollapseAll(expandGroupId, childFeedbackIds) && ( panelCollapseHelper.setAllCollapsed(parentId, childFeedbackIds)} + onClick={() => panelCollapseHelper.setAllCollapsed(expandGroupId, childFeedbackIds)} title="Collapse all feedbacks" > @@ -177,22 +183,22 @@ const InlineFeedbacksEditor = observer(function InlineFeedbacksEditor({ ))} - {!!parentId && ( + {!!ownerId && ( @@ -201,7 +207,7 @@ const InlineFeedbacksEditor = observer(function InlineFeedbacksEditor({
- + (null) @@ -263,31 +269,31 @@ function FeedbackTableRow({ // Ensure the hover targets this element, and not a child element if (!monitor.isOver({ shallow: true })) return - const dragParentId = item.parentId + const dragOwnerId = item.ownerId const dragIndex = item.index - const hoverParentId = parentId + const hoverOwnerId = ownerId const hoverIndex = index const hoverId = feedback.id if (!checkDragState(item, monitor, hoverId)) return // Don't replace items with themselves - if (item.feedbackId === hoverId || (dragIndex === hoverIndex && dragParentId === hoverParentId)) { + if (item.feedbackId === hoverId || (dragIndex === hoverIndex && isEqual(dragOwnerId, hoverOwnerId))) { return } // Can't move into itself - if (item.feedbackId === hoverParentId) return + if (hoverOwnerId && item.feedbackId === hoverOwnerId.parentFeedbackId) return // Time to actually perform the action - serviceFactory.moveCard(item.feedbackId, hoverParentId, hoverIndex) + serviceFactory.moveCard(item.feedbackId, hoverOwnerId, hoverIndex) // Note: we're mutating the monitor item here! // Generally it's better to avoid mutations, // but it's good here for the sake of performance // to avoid expensive index searches. item.index = hoverIndex - item.parentId = hoverParentId + item.ownerId = hoverOwnerId }, drop(item, _monitor) { item.dragState = null @@ -298,7 +304,7 @@ function FeedbackTableRow({ item: { feedbackId: feedback.id, index: index, - parentId: parentId, + ownerId: ownerId, dragState: null, }, collect: (monitor) => ({ @@ -320,13 +326,13 @@ function FeedbackTableRow({ @@ -335,24 +341,24 @@ function FeedbackTableRow({ interface FeedbackEditorProps { controlId: string - parentId: string | null + ownerId: FeedbackOwner | null entityType: string feedback: FeedbackInstance location: ControlLocation | undefined serviceFactory: IFeedbackEditorService panelCollapseHelper: PanelCollapseHelper - booleanOnly: boolean + onlyType: 'boolean' | 'advanced' | null } const FeedbackEditor = observer(function FeedbackEditor({ controlId, - parentId, + ownerId, entityType, feedback, location, serviceFactory, panelCollapseHelper, - booleanOnly, + onlyType, }: FeedbackEditorProps) { const service = useControlFeedbackService(serviceFactory, feedback) @@ -390,7 +396,10 @@ const FeedbackEditor = observer(function FeedbackEditor({ () => panelCollapseHelper.setPanelCollapsed(feedback.id, false), [panelCollapseHelper, feedback.id] ) - const isCollapsed = panelCollapseHelper.isPanelCollapsed(parentId, feedback.id) + const isCollapsed = panelCollapseHelper.isPanelCollapsed(feedbackOwnerString(ownerId), feedback.id) + + const childrenGroupId: FeedbackOwner = { parentFeedbackId: feedback.id, childGroup: 'children' } + const advancedChildrenGroupId: FeedbackOwner = { parentFeedbackId: feedback.id, childGroup: 'advancedChildren' } return ( <> @@ -498,17 +507,36 @@ const FeedbackEditor = observer(function FeedbackEditor({ + + {feedbackSpec.supportsAdvancedChildFeedbacks && ( + <> + + + + + )}
)} @@ -548,7 +576,7 @@ const FeedbackEditor = observer(function FeedbackEditor({ )} - {!booleanOnly && ( + {onlyType === null && ( <> void + moveCard: (dragFeedbackId: string, hoverOwnerId: FeedbackOwner | null, hoverIndex: number) => void } -function FeedbackRowDropPlaceholder({ dragId, parentId, feedbackCount, moveCard }: FeedbackRowDropPlaceholderProps) { +function FeedbackRowDropPlaceholder({ dragId, ownerId, feedbackCount, moveCard }: FeedbackRowDropPlaceholderProps) { const [isDragging, drop] = useDrop({ accept: dragId, collect: (monitor) => { @@ -665,9 +693,9 @@ function FeedbackRowDropPlaceholder({ dragId, parentId, feedbackCount, moveCard }, hover(item, _monitor) { // Can't move into itself - if (item.feedbackId === parentId) return + if (isEqual(item.feedbackId, ownerId)) return - moveCard(item.feedbackId, parentId, 0) + moveCard(item.feedbackId, ownerId, 0) }, }) @@ -681,3 +709,7 @@ function FeedbackRowDropPlaceholder({ dragId, parentId, feedbackCount, moveCard ) } + +function feedbackOwnerString(ownerId: FeedbackOwner | null): string | null { + return ownerId ? `${ownerId.parentFeedbackId}_${ownerId.childGroup}` : null +} diff --git a/webui/src/Services/Controls/ControlFeedbacksService.ts b/webui/src/Services/Controls/ControlFeedbacksService.ts index 193273f60..23aa55641 100644 --- a/webui/src/Services/Controls/ControlFeedbacksService.ts +++ b/webui/src/Services/Controls/ControlFeedbacksService.ts @@ -1,11 +1,11 @@ import { useContext, useMemo, useRef } from 'react' import { SocketContext, socketEmitPromise } from '../../util.js' -import { FeedbackInstance } from '@companion-app/shared/Model/FeedbackModel.js' +import { FeedbackInstance, FeedbackOwner } from '@companion-app/shared/Model/FeedbackModel.js' import { GenericConfirmModalRef } from '../../Components/GenericConfirmModal.js' export interface IFeedbackEditorService { - addFeedback: (feedbackType: string, parentId: string | null) => void - moveCard: (dragId: string, hoverParentId: string | null, hoverIndex: number) => void + addFeedback: (feedbackType: string, ownerId: FeedbackOwner | null) => void + moveCard: (dragId: string, hoverOwnerId: FeedbackOwner | null, hoverIndex: number) => void setValue: (feedbackId: string, feedback: FeedbackInstance | undefined, key: string, value: any) => void setConnection: (feedbackId: string, connectionId: string | number) => void @@ -41,22 +41,19 @@ export function useControlFeedbacksEditorService( return useMemo( () => ({ - addFeedback: (feedbackType: string, parentId: string | null) => { + addFeedback: (feedbackType: string, ownerId: FeedbackOwner | null) => { const [connectionId, feedbackId] = feedbackType.split(':', 2) - socketEmitPromise(socket, 'controls:feedback:add', [ - controlId, - parentId ? { parentFeedbackId: parentId, childGroup: 'children' } : null, - connectionId, - feedbackId, - ]).catch((e) => { - console.error('Failed to add control feedback', e) - }) + socketEmitPromise(socket, 'controls:feedback:add', [controlId, ownerId, connectionId, feedbackId]).catch( + (e) => { + console.error('Failed to add control feedback', e) + } + ) }, - moveCard: (dragFeedbackId: string, hoverParentId: string | null, hoverIndex: number) => { + moveCard: (dragFeedbackId: string, hoverOwnerId: FeedbackOwner | null, hoverIndex: number) => { socketEmitPromise(socket, 'controls:feedback:move', [ controlId, dragFeedbackId, - hoverParentId ? { parentFeedbackId: hoverParentId, childGroup: 'children' } : null, + hoverOwnerId, hoverIndex, ]).catch((e) => { console.error(`Move failed: ${e}`) diff --git a/webui/src/Triggers/EditPanel.tsx b/webui/src/Triggers/EditPanel.tsx index 68dbf53ef..a449ffdb0 100644 --- a/webui/src/Triggers/EditPanel.tsx +++ b/webui/src/Triggers/EditPanel.tsx @@ -152,7 +152,7 @@ export function EditTriggerPanel({ controlId }: EditTriggerPanelProps) { entityType="condition" controlId={controlId} feedbacks={config.condition} - booleanOnly={true} + onlyType={'boolean'} location={undefined} addPlaceholder="+ Add condition" /> From f81c6c0b7457a7040d6eff873146ab709c20e6d0 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Dec 2024 21:56:49 +0000 Subject: [PATCH 4/6] wip: ux --- webui/src/Controls/FeedbackEditor.tsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/webui/src/Controls/FeedbackEditor.tsx b/webui/src/Controls/FeedbackEditor.tsx index 3acafb380..8e63d8bb1 100644 --- a/webui/src/Controls/FeedbackEditor.tsx +++ b/webui/src/Controls/FeedbackEditor.tsx @@ -7,6 +7,7 @@ import { faCopy, faFolderOpen, faPencil, + faQuestionCircle, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import React, { useCallback, useContext, useMemo, useRef, useState } from 'react' @@ -507,7 +508,17 @@ const FeedbackEditor = observer(function FeedbackEditor({ + Conditions  + + + ) : null + } feedbacks={feedback.children ?? []} entityType="condition" onlyType={'boolean'} @@ -521,10 +532,18 @@ const FeedbackEditor = observer(function FeedbackEditor({ {feedbackSpec.supportsAdvancedChildFeedbacks && ( <> - + + Feedbacks  + + + } feedbacks={feedback.advancedChildren ?? []} entityType="feedback" onlyType={'advanced'} From 2ffa9e6769cb88ea685d373f74d2b92126aab1d8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 18:03:44 +0000 Subject: [PATCH 5/6] fix: fixup feedbacks on import --- companion/lib/ImportExport/Controller.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/companion/lib/ImportExport/Controller.ts b/companion/lib/ImportExport/Controller.ts index 2e4c95c7a..33d26a654 100644 --- a/companion/lib/ImportExport/Controller.ts +++ b/companion/lib/ImportExport/Controller.ts @@ -1182,6 +1182,10 @@ function fixupFeedbacksRecursive(instanceIdMap: InstanceAppliedRemappings, feedb feedback.instance_id === 'internal' && feedback.children ? fixupFeedbacksRecursive(instanceIdMap, feedback.children) : undefined, + advancedChildren: + feedback.instance_id === 'internal' && feedback.advancedChildren + ? fixupFeedbacksRecursive(instanceIdMap, feedback.advancedChildren) + : undefined, }) } } From dfdf985a5e4ade44db4003c7fd27c30e3abbc63f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 19 Dec 2024 21:15:40 +0000 Subject: [PATCH 6/6] wip --- .../lib/Controls/Fragments/FragmentFeedbackInstance.ts | 2 +- companion/lib/Controls/Fragments/FragmentFeedbacks.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts b/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts index ab5433920..6624278a3 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbackInstance.ts @@ -28,7 +28,7 @@ export class FragmentFeedbackInstance { */ readonly #controlId: string - readonly #data: Omit + readonly #data: Omit #children = new Map() diff --git a/companion/lib/Controls/Fragments/FragmentFeedbacks.ts b/companion/lib/Controls/Fragments/FragmentFeedbacks.ts index cb92b200c..b82d6dd95 100644 --- a/companion/lib/Controls/Fragments/FragmentFeedbacks.ts +++ b/companion/lib/Controls/Fragments/FragmentFeedbacks.ts @@ -424,7 +424,7 @@ export class FragmentFeedbacks { * Get all the feedback instances * @param onlyConnectionId Optionally, only for a specific connection */ - getFlattenedFeedbackInstances(onlyConnectionId?: string): Omit[] { + getFlattenedFeedbackInstances(onlyConnectionId?: string): Omit[] { const instances: FeedbackInstance[] = [] const extractInstances = (feedbacks: FeedbackInstance[]) => { @@ -433,12 +433,12 @@ export class FragmentFeedbacks { instances.push({ ...feedback, children: undefined, + advancedChildren: undefined, }) } - if (feedback.children) { - extractInstances(feedback.children) - } + if (feedback.children) extractInstances(feedback.children) + if (feedback.advancedChildren) extractInstances(feedback.advancedChildren) } }