From c264c65e3c28462690684f6bbe6630661ddbac1d Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 23 Jan 2026 12:03:57 +0100 Subject: [PATCH] feat(EAV-663): allow blueprints to specify preview and thumbnail containter ids in `applyConfig` --- meteor/__mocks__/defaultCollectionObjects.ts | 6 +- meteor/server/__tests__/cronjobs.test.ts | 6 +- meteor/server/api/rest/v1/typeConversion.ts | 6 +- meteor/server/api/studio/api.ts | 6 +- meteor/server/migration/0_1_0.ts | 6 +- meteor/server/migration/X_X_X.ts | 8 +- .../migration/__tests__/migrations.test.ts | 136 +++++++----------- meteor/server/migration/databaseMigration.ts | 14 +- ...erIdsToObjectWithOverridesMigrationStep.ts | 54 +++++++ ...ToObjectWithOverridesMigrationStep.test.ts | 73 ++++++++++ .../expectedPackages/generate.ts | 20 +-- .../expectedPackages/publication.ts | 13 +- .../__tests__/checkPieceContentStatus.test.ts | 6 +- .../checkPieceContentStatus.ts | 10 +- .../pieceContentStatusUI/common.ts | 9 +- .../blueprints-integration/src/api/studio.ts | 7 +- packages/corelib/src/dataModel/Studio.ts | 10 +- .../src/__mocks__/defaultCollectionObjects.ts | 6 +- packages/job-worker/src/playout/upgrade.ts | 1 + .../src/collections/ExpectedPackages.ts | 8 +- .../src/core/model/PackageContainer.ts | 5 + .../src/__mocks__/defaultCollectionObjects.ts | 6 +- .../PackageContainerPickers.tsx | 114 +++++++++++---- 23 files changed, 358 insertions(+), 172 deletions(-) create mode 100644 meteor/server/migration/steps/ContainerIdsToObjectWithOverridesMigrationStep.ts create mode 100644 meteor/server/migration/steps/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index fa8a8934ed..c44b5be19a 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -116,8 +116,10 @@ export function defaultStudio(_id: StudioId): DBStudio { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerIdsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index c61e36bdcb..cf33580740 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -592,8 +592,10 @@ describe('cronjobs', () => { routeSetsWithOverrides: newObjectWithOverrides({}), routeSetExclusivityGroupsWithOverrides: newObjectWithOverrides({}), packageContainersWithOverrides: newObjectWithOverrides({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerIdsWithOverrides: newObjectWithOverrides({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: newObjectWithOverrides({}), ingestDevices: newObjectWithOverrides({}), diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index 9742c5815b..eeb46a6e0b 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -359,8 +359,10 @@ export async function buildStudioFromResolved({ _rundownVersionHash: '', routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerIdsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 0772e382fe..9f9acb2416 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -69,8 +69,10 @@ export async function insertStudioInner(newId?: StudioId): Promise { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - thumbnailContainerIds: [], - previewContainerIds: [], + packageContainerIdsWithOverrides: wrapDefaultObject({ + thumbnailContainerIds: [], + previewContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index 8b5e4e5854..323621df05 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -44,8 +44,10 @@ export const addSteps = addMigrationSteps('0.1.0', [ routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - thumbnailContainerIds: [], - previewContainerIds: [], + packageContainerIdsWithOverrides: wrapDefaultObject({ + thumbnailContainerIds: [], + previewContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 30a74d769e..18f31aefd6 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -10,6 +10,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { BucketId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { assertNever, Complete } from '@sofie-automation/corelib/dist/lib' +import { ContainerIdsToObjectWithOverridesMigrationStep } from './steps/ContainerIdsToObjectWithOverridesMigrationStep' /* * ************************************************************************************** @@ -36,7 +37,9 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ ['expectedMediaItems', 'mediaWorkFlows', 'mediaWorkFlowSteps'].includes(c.name) ) if (collectionsToDrop.length > 0) { - return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop.map((c) => c.name).join(', ')}` + return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop + .map((c) => c.name) + .join(', ')}` } return false @@ -195,4 +198,7 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ } }, }, + // Add your migration here + + new ContainerIdsToObjectWithOverridesMigrationStep(), ]) diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 5252a28081..e41ead05a1 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -1,18 +1,17 @@ import _ from 'underscore' -import { setupEmptyEnvironment } from '../../../__mocks__/helpers/database' +import { setupEmptyEnvironment, setupMockStudio } from '../../../__mocks__/helpers/database' import { ICoreSystem, GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { clearMigrationSteps, addMigrationSteps, prepareMigration, PreparedMigration } from '../databaseMigration' import { CURRENT_SYSTEM_VERSION } from '../currentSystemVersion' import { RunMigrationResult, GetMigrationStatusResult } from '@sofie-automation/meteor-lib/dist/api/migration' import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' +import { MigrationStepCore, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { MeteorCall } from '../../api/methods' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { ShowStyleBases, ShowStyleVariants, Studios } from '../../collections' import { getCoreSystemAsync } from '../../coreSystem/collection' -import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' import fs from 'fs' require('../../api/peripheralDevice.ts') // include in order to create the Meteor methods needed @@ -107,35 +106,8 @@ describe('Migrations', () => { return false }, migrate: async () => { - await Studios.insertAsync({ + await setupMockStudio({ _id: protectString('studioMock2'), - name: 'Default studio', - supportedShowStyleBase: [], - settingsWithOverrides: wrapDefaultObject({ - mediaPreviewsUrl: '', - frameRate: 25, - minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, - allowHold: true, - allowPieceDirectPlay: true, - enableBuckets: true, - enableEvaluationForm: true, - }), - mappingsWithOverrides: wrapDefaultObject({}), - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - routeSetsWithOverrides: wrapDefaultObject({}), - routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], - peripheralDeviceSettings: { - deviceSettings: wrapDefaultObject({}), - playoutDevices: wrapDefaultObject({}), - ingestDevices: wrapDefaultObject({}), - inputDevices: wrapDefaultObject({}), - }, - lastBlueprintConfig: undefined, - lastBlueprintFixUpHash: undefined, }) }, }, @@ -149,35 +121,8 @@ describe('Migrations', () => { return false }, migrate: async () => { - await Studios.insertAsync({ + await setupMockStudio({ _id: protectString('studioMock3'), - name: 'Default studio', - supportedShowStyleBase: [], - settingsWithOverrides: wrapDefaultObject({ - mediaPreviewsUrl: '', - frameRate: 25, - minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, - allowHold: true, - allowPieceDirectPlay: true, - enableBuckets: true, - enableEvaluationForm: true, - }), - mappingsWithOverrides: wrapDefaultObject({}), - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - routeSetsWithOverrides: wrapDefaultObject({}), - routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], - peripheralDeviceSettings: { - deviceSettings: wrapDefaultObject({}), - playoutDevices: wrapDefaultObject({}), - ingestDevices: wrapDefaultObject({}), - inputDevices: wrapDefaultObject({}), - }, - lastBlueprintConfig: undefined, - lastBlueprintFixUpHash: undefined, }) }, }, @@ -191,35 +136,8 @@ describe('Migrations', () => { return false }, migrate: async () => { - await Studios.insertAsync({ + await setupMockStudio({ _id: protectString('studioMock1'), - name: 'Default studio', - supportedShowStyleBase: [], - settingsWithOverrides: wrapDefaultObject({ - mediaPreviewsUrl: '', - frameRate: 25, - minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, - allowHold: true, - allowPieceDirectPlay: true, - enableBuckets: true, - enableEvaluationForm: true, - }), - mappingsWithOverrides: wrapDefaultObject({}), - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - routeSetsWithOverrides: wrapDefaultObject({}), - routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], - peripheralDeviceSettings: { - deviceSettings: wrapDefaultObject({}), - playoutDevices: wrapDefaultObject({}), - ingestDevices: wrapDefaultObject({}), - inputDevices: wrapDefaultObject({}), - }, - lastBlueprintConfig: undefined, - lastBlueprintFixUpHash: undefined, }) }, }, @@ -322,4 +240,48 @@ describe('Migrations', () => { expect(steps.indexOf(myShowStyleMockStep3)).toEqual(8) */ }) + + test('Class-based migration steps work with proper binding', async () => { + await MeteorCall.migration.resetDatabaseVersions() + clearMigrationSteps() + + // Create a migration step class that uses instance properties + class TestClassMigrationStep implements Omit { + public readonly id = 'classBasedMigrationTest' + public readonly canBeRunAutomatically = true + public testValue = 'initialized' + + public async validate(): Promise { + // If 'this' is not bound, testValue will be undefined + return this.testValue === 'initialized' ? 'Migration needed' : false + } + + public async migrate(): Promise { + // If 'this' is not bound, this will throw or fail to update the correct instance + this.testValue = 'migrated' + } + } + + // Instantiate the step so we can check it later + const step = new TestClassMigrationStep() + addMigrationSteps('1.0.0', [step])() + + // Prepare migration to ensure it's detected + const migration = await prepareMigration(true) + expect(migration.migrationNeeded).toEqual(true) + expect(_.find(migration.steps, (s) => s.id === 'classBasedMigrationTest')).toBeTruthy() + + // Run the migration to verify that methods are properly bound + const migrationStatus: GetMigrationStatusResult = await MeteorCall.migration.getMigrationStatus() + const migrationResult: RunMigrationResult = await MeteorCall.migration.runMigration( + migrationStatus.migration.chunks, + migrationStatus.migration.hash, + userInput(migrationStatus) + ) + + expect(migrationResult.migrationCompleted).toEqual(true) + + // Verify that migrate() was called and 'this' was correctly bound + expect(step.testValue).toEqual('migrated') + }) }) diff --git a/meteor/server/migration/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index eed1dc3948..f2d54d9ef2 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -82,10 +82,8 @@ const coreMigrationSteps: Array = [] export function addMigrationSteps(version: string, steps: Array>) { return (): void => { for (const step of steps) { - coreMigrationSteps.push({ - ...step, - version: version, - }) + ;(step as MigrationStepCore).version = version + coreMigrationSteps.push(step as MigrationStepCore) } } } @@ -135,9 +133,9 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise { + public readonly id = `convert previewContainerIds to ObjectWithOverrides` + public readonly canBeRunAutomatically = true + + public async validate(): Promise { + const studios = await this.findStudiosToMigrate() + + if (studios.length) { + return 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides' + } + + return false + } + + public async migrate(): Promise { + const studios = await this.findStudiosToMigrate() + + for (const studio of studios) { + // @ts-expect-error previewContainerIds is typed as string[] + const oldPreviewContainerIds = studio.previewContainerIds + // @ts-expect-error thumbnailContainerIds is typed as string[] + const oldThumbnailContainerIds = studio.thumbnailContainerIds + + const newPackageContainers = convertObjectIntoOverrides({ + previewContainerIds: oldPreviewContainerIds ?? [], + thumbnailContainerIds: oldThumbnailContainerIds ?? [], + } satisfies StudioPackageContainerIds) as ObjectWithOverrides + + await Studios.updateAsync(studio._id, { + $set: { + packageContainerIdsWithOverrides: newPackageContainers, + }, + $unset: { + previewContainerIds: 1, + thumbnailContainerIds: 1, + }, + }) + } + } + + private async findStudiosToMigrate() { + return Studios.findFetchAsync({ + packageContainerIdsWithOverrides: { $exists: false }, + }) + } +} diff --git a/meteor/server/migration/steps/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts b/meteor/server/migration/steps/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts new file mode 100644 index 0000000000..50fad987d6 --- /dev/null +++ b/meteor/server/migration/steps/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts @@ -0,0 +1,73 @@ +import { setupEmptyEnvironment, setupMockStudio } from '../../../../__mocks__/helpers/database' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { Studios } from '../../../collections' +import { ContainerIdsToObjectWithOverridesMigrationStep } from '../ContainerIdsToObjectWithOverridesMigrationStep' + +describe('ContainerIdsToObjectWithOverridesMigrationStep', () => { + beforeEach(async () => { + await setupEmptyEnvironment() + }) + + test('migration is needed when studio is missing packageContainerIdsWithOverrides', async () => { + await setupMockStudio({ + _id: protectString('studio0'), + // @ts-expect-error + previewContainerIds: ['preview1'], + thumbnailContainerIds: ['thumb1'], + packageContainerIdsWithOverrides: undefined as any, + }) + + const step = new ContainerIdsToObjectWithOverridesMigrationStep() + const validateResult = await step.validate() + expect(validateResult).toBe( + 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides' + ) + + await step.migrate() + + const studio = await Studios.findOneAsync(protectString('studio0')) + expect(studio).toBeTruthy() + expect(studio?.packageContainerIdsWithOverrides).toMatchObject({ + defaults: {}, + overrides: [ + { op: 'set', path: 'previewContainerIds', value: ['preview1'] }, + { op: 'set', path: 'thumbnailContainerIds', value: ['thumb1'] }, + ], + }) + // @ts-expect-error + expect(studio?.previewContainerIds).toBeUndefined() + // @ts-expect-error + expect(studio?.thumbnailContainerIds).toBeUndefined() + + const validateResultAfter = await step.validate() + expect(validateResultAfter).toBe(false) + }) + + test('migration handles missing optional old fields', async () => { + await setupMockStudio({ + _id: protectString('studio1'), + packageContainerIdsWithOverrides: undefined as any, + }) + + const step = new ContainerIdsToObjectWithOverridesMigrationStep() + const validateResult = await step.validate() + expect(validateResult).toBe( + 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides' + ) + + await step.migrate() + + const studio = await Studios.findOneAsync(protectString('studio1')) + expect(studio).toBeTruthy() + expect(studio?.packageContainerIdsWithOverrides).toMatchObject({ + defaults: {}, + overrides: [ + { op: 'set', path: 'previewContainerIds', value: [] }, + { op: 'set', path: 'thumbnailContainerIds', value: [] }, + ], + }) + + const validateResultAfter = await step.validate() + expect(validateResultAfter).toBe(false) + }) +}) diff --git a/meteor/server/publications/packageManager/expectedPackages/generate.ts b/meteor/server/publications/packageManager/expectedPackages/generate.ts index 5c815af910..7e2f0a0cfd 100644 --- a/meteor/server/publications/packageManager/expectedPackages/generate.ts +++ b/meteor/server/publications/packageManager/expectedPackages/generate.ts @@ -3,6 +3,7 @@ import { Accessor, AccessorOnPackage, ExpectedPackage, + StudioPackageContainerIds, } from '@sofie-automation/blueprints-integration' import { PeripheralDeviceId, ExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' @@ -15,12 +16,11 @@ import deepExtend from 'deep-extend' import { ReadonlyDeep } from 'type-fest' import _ from 'underscore' import { getSideEffect } from '@sofie-automation/meteor-lib/dist/collections/ExpectedPackages' -import { DBStudio, StudioLight, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone, omit } from '@sofie-automation/corelib/dist/lib' import { CustomPublishCollection } from '../../../lib/customPublication' import { logger } from '../../../logging' import { ExpectedPackageDBCompact, ExpectedPackagesContentCache } from './contentCache' -import type { StudioFields } from './publication' /** * Regenerate the output for the provided ExpectedPackage `regenerateIds`, updating the data in `collection` as needed @@ -33,7 +33,7 @@ import type { StudioFields } from './publication' */ export async function updateCollectionForExpectedPackageIds( contentCache: ReadonlyDeep, - studio: Pick, + packageContainerIds: StudioPackageContainerIds, layerNameToDeviceIds: Map, packageContainers: Record, collection: CustomPublishCollection, @@ -63,7 +63,12 @@ export async function updateCollectionForExpectedPackageIds( // Filter, keep only the routed mappings for this device: if (filterPlayoutDeviceIds && !filterPlayoutDeviceIds.includes(deviceId)) continue - const routedPackage = generateExpectedPackageForDevice(studio, packageDoc, deviceId, packageContainers) + const routedPackage = generateExpectedPackageForDevice( + packageContainerIds, + packageDoc, + deviceId, + packageContainers + ) updatedDocIds.add(routedPackage._id) collection.replace(routedPackage) @@ -81,10 +86,7 @@ export async function updateCollectionForExpectedPackageIds( } function generateExpectedPackageForDevice( - studio: Pick< - StudioLight, - '_id' | 'packageContainersWithOverrides' | 'previewContainerIds' | 'thumbnailContainerIds' - >, + packageContainerIds: StudioPackageContainerIds, expectedPackage: ExpectedPackageDBCompact, deviceId: PeripheralDeviceId, packageContainers: Record @@ -118,7 +120,7 @@ function generateExpectedPackageForDevice( if (!combinedTargets.length) { logger.warn(`Pub.expectedPackagesForDevice: No targets found for "${expectedPackage._id}"`) } - const packageSideEffect = getSideEffect(expectedPackage.package, studio) + const packageSideEffect = getSideEffect(expectedPackage.package, packageContainerIds) return { _id: protectString(`${expectedPackage._id}_${deviceId}`), diff --git a/meteor/server/publications/packageManager/expectedPackages/publication.ts b/meteor/server/publications/packageManager/expectedPackages/publication.ts index 46328b5ce8..b81ac1d6dd 100644 --- a/meteor/server/publications/packageManager/expectedPackages/publication.ts +++ b/meteor/server/publications/packageManager/expectedPackages/publication.ts @@ -30,6 +30,7 @@ import { PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { checkAccessAndGetPeripheralDevice } from '../../../security/check' +import { StudioPackageContainerIds } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' interface ExpectedPackagesPublicationArgs { readonly studioId: StudioId @@ -50,6 +51,7 @@ interface ExpectedPackagesPublicationState { studio: Pick | undefined layerNameToDeviceIds: Map packageContainers: Record + packageContainerIds: StudioPackageContainerIds contentCache: ReadonlyDeep } @@ -59,15 +61,13 @@ export type StudioFields = | 'routeSetsWithOverrides' | 'mappingsWithOverrides' | 'packageContainersWithOverrides' - | 'previewContainerIds' - | 'thumbnailContainerIds' + | 'packageContainerIdsWithOverrides' const studioFieldSpecifier = literal>>({ _id: 1, routeSetsWithOverrides: 1, mappingsWithOverrides: 1, packageContainersWithOverrides: 1, - previewContainerIds: 1, - thumbnailContainerIds: 1, + packageContainerIdsWithOverrides: 1, }) async function setupExpectedPackagesPublicationObservers( @@ -125,6 +125,7 @@ async function manipulateExpectedPackagesPublicationData( if (!state.layerNameToDeviceIds) state.layerNameToDeviceIds = new Map() if (!state.packageContainers) state.packageContainers = {} + if (!state.packageContainerIds) state.packageContainerIds = { previewContainerIds: [], thumbnailContainerIds: [] } if (invalidateAllItems) { // Everything is invalid, reset everything @@ -145,6 +146,7 @@ async function manipulateExpectedPackagesPublicationData( logger.warn(`Pub.expectedPackagesForDevice: studio "${args.studioId}" not found!`) state.layerNameToDeviceIds = new Map() state.packageContainers = {} + state.packageContainerIds = { previewContainerIds: [], thumbnailContainerIds: [] } } else { const studioMappings = applyAndValidateOverrides(state.studio.mappingsWithOverrides).obj state.layerNameToDeviceIds = buildMappingsToDeviceIdMap( @@ -152,6 +154,7 @@ async function manipulateExpectedPackagesPublicationData( studioMappings ) state.packageContainers = applyAndValidateOverrides(state.studio.packageContainersWithOverrides).obj + state.packageContainerIds = applyAndValidateOverrides(state.studio.packageContainerIdsWithOverrides).obj } } @@ -173,7 +176,7 @@ async function manipulateExpectedPackagesPublicationData( await updateCollectionForExpectedPackageIds( state.contentCache, - state.studio, + state.packageContainerIds, state.layerNameToDeviceIds, state.packageContainers, collection, diff --git a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts index f6a8069a8e..e2fed5e60d 100644 --- a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts +++ b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts @@ -261,8 +261,10 @@ describe('lib/mediaObjects', () => { const mockStudio: Complete = { _id: mockDefaultStudio._id, settings: mockStudioSettings, - previewContainerIds: ['previews0'], - thumbnailContainerIds: ['thumbnails0'], + packageContainerIds: { + previewContainerIds: ['previews0'], + thumbnailContainerIds: ['thumbnails0'], + }, routeSets: applyAndValidateOverrides(mockDefaultStudio.routeSetsWithOverrides).obj, mappings: applyAndValidateOverrides(mockDefaultStudio.mappingsWithOverrides).obj, packageContainers: applyAndValidateOverrides(mockDefaultStudio.packageContainersWithOverrides).obj, diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index 5aa69a23db..ed4e811d71 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -57,6 +57,7 @@ import { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/ import { PieceContentStatusMessageFactory, PieceContentStatusMessageRequiredArgs } from './messageFactory' import { PackageStatusMessage } from '@sofie-automation/shared-lib/dist/packageStatusMessages' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import { StudioPackageContainerIds } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' const DEFAULT_MESSAGE_FACTORY = new PieceContentStatusMessageFactory(undefined) @@ -211,8 +212,7 @@ export type PieceContentStatusPiece = Pick< */ previousPieceInstanceId?: PieceInstanceId } -export interface PieceContentStatusStudio - extends Pick { +export interface PieceContentStatusStudio extends Pick { /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt /** Route sets with overrides */ @@ -222,6 +222,8 @@ export interface PieceContentStatusStudio */ packageContainers: Record + packageContainerIds: StudioPackageContainerIds + settings: IStudioSettings } @@ -706,7 +708,7 @@ async function checkPieceContentExpectedPackageStatus( } if (!thumbnailUrl) { - const sideEffect = getSideEffect(expectedPackage, studio) + const sideEffect = getSideEffect(expectedPackage, studio.packageContainerIds) thumbnailUrl = await getAssetUrlFromPackageContainerStatus( studio.packageContainers, @@ -718,7 +720,7 @@ async function checkPieceContentExpectedPackageStatus( } if (!previewUrl) { - const sideEffect = getSideEffect(expectedPackage, studio) + const sideEffect = getSideEffect(expectedPackage, studio.packageContainerIds) previewUrl = await getAssetUrlFromPackageContainerStatus( studio.packageContainers, diff --git a/meteor/server/publications/pieceContentStatusUI/common.ts b/meteor/server/publications/pieceContentStatusUI/common.ts index f8c7591d53..95e8ed9d7a 100644 --- a/meteor/server/publications/pieceContentStatusUI/common.ts +++ b/meteor/server/publications/pieceContentStatusUI/common.ts @@ -15,16 +15,14 @@ export type StudioFields = | '_id' | 'settingsWithOverrides' | 'packageContainersWithOverrides' - | 'previewContainerIds' - | 'thumbnailContainerIds' + | 'packageContainerIdsWithOverrides' | 'mappingsWithOverrides' | 'routeSetsWithOverrides' export const studioFieldSpecifier = literal>>({ _id: 1, settingsWithOverrides: 1, packageContainersWithOverrides: 1, - previewContainerIds: 1, - thumbnailContainerIds: 1, + packageContainerIdsWithOverrides: 1, mappingsWithOverrides: 1, routeSetsWithOverrides: 1, }) @@ -113,8 +111,7 @@ export async function fetchStudio(studioId: StudioId): Promise /** Package Containers */ packageContainers?: Record + /** Which Package Containers are used for media previews/thumbnails in GUI */ + packageContainerIds?: StudioPackageContainerIds studioSettings?: IStudioSettings } diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index 0f1722f585..4400ace149 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -13,7 +13,10 @@ import { StudioRouteType, StudioAbPlayerDisabling, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' -import { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import { + StudioPackageContainer, + StudioPackageContainerIds, +} from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' import { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' export { MappingsExt, MappingExt, MappingsHash, IStudioSettings } @@ -74,9 +77,8 @@ export interface DBStudio { */ packageContainersWithOverrides: ObjectWithOverrides> - /** Which package containers is used for media previews in GUI */ - previewContainerIds: string[] - thumbnailContainerIds: string[] + /** Which package containers are used for media previews/thumbnails in GUI */ + packageContainerIdsWithOverrides: ObjectWithOverrides peripheralDeviceSettings: StudioPeripheralDeviceSettings diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 88869a4da8..e4515897d0 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -111,8 +111,10 @@ export function defaultStudio(_id: StudioId): DBStudio { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerIdsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index aa95875a72..3089a4e655 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -198,6 +198,7 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data 'peripheralDeviceSettings.inputDevices.defaults': inputDevices, 'routeSetsWithOverrides.defaults': routeSets, 'routeSetExclusivityGroupsWithOverrides.defaults': routeSetExclusivityGroups, + 'packageContainerIdsWithOverrides.defaults': result.packageContainerIds, 'packageContainersWithOverrides.defaults': packageContainers, lastBlueprintConfig: { blueprintHash: blueprint.blueprintDoc.blueprintHash, diff --git a/packages/meteor-lib/src/collections/ExpectedPackages.ts b/packages/meteor-lib/src/collections/ExpectedPackages.ts index 5815971453..26826ab6d9 100644 --- a/packages/meteor-lib/src/collections/ExpectedPackages.ts +++ b/packages/meteor-lib/src/collections/ExpectedPackages.ts @@ -1,12 +1,12 @@ import { ExpectedPackage } from '@sofie-automation/blueprints-integration' import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' -import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' import deepExtend from 'deep-extend' import { htmlTemplateGetSteps, htmlTemplateGetFileNamesFromSteps, } from '@sofie-automation/shared-lib/dist/package-manager/helpers' import { ReadonlyDeep } from 'type-fest' +import { StudioPackageContainerIds } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' export function getPreviewPackageSettings( expectedPackage: ExpectedPackage.Any @@ -57,13 +57,13 @@ export function getThumbnailPackageSettings( } export function getSideEffect( expectedPackage: ReadonlyDeep, - studio: Pick + packageContainerIds: StudioPackageContainerIds ): ExpectedPackage.Base['sideEffect'] { return deepExtend( {}, literal({ - previewContainerId: studio.previewContainerIds[0], // just pick the first. Todo: something else? - thumbnailContainerId: studio.thumbnailContainerIds[0], // just pick the first. Todo: something else? + previewContainerId: packageContainerIds.previewContainerIds[0], // just pick the first. Todo: something else? + thumbnailContainerId: packageContainerIds.thumbnailContainerIds[0], // just pick the first. Todo: something else? previewPackageSettings: getPreviewPackageSettings(expectedPackage as ExpectedPackage.Any), thumbnailPackageSettings: getThumbnailPackageSettings(expectedPackage as ExpectedPackage.Any), }), diff --git a/packages/shared-lib/src/core/model/PackageContainer.ts b/packages/shared-lib/src/core/model/PackageContainer.ts index 9283ed32d3..3d1ca50112 100644 --- a/packages/shared-lib/src/core/model/PackageContainer.ts +++ b/packages/shared-lib/src/core/model/PackageContainer.ts @@ -5,3 +5,8 @@ export interface StudioPackageContainer { deviceIds: string[] container: PackageContainer } + +export interface StudioPackageContainerIds { + previewContainerIds: string[] + thumbnailContainerIds: string[] +} diff --git a/packages/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index 7434f499fb..e1b5a7ebb4 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -111,8 +111,10 @@ export function defaultStudio(_id: StudioId): DBStudio { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerIdsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx index 8c107c0c0f..82285c2abe 100644 --- a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx @@ -1,12 +1,23 @@ import * as React from 'react' import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { EditAttribute } from '../../../../lib/EditAttribute.js' import { useTranslation } from 'react-i18next' import { Accessor } from '@sofie-automation/blueprints-integration' import { Studios } from '../../../../collections/index.js' import { DropdownInputOption } from '../../../../lib/Components/DropdownInput.js' -import { WrappedOverridableItem } from '../../util/OverrideOpHelper.js' -import { LabelActual } from '../../../../lib/Components/LabelAndOverrides.js' +import { + useOverrideOpHelper, + WrappedOverridableItem, + WrappedOverridableItemNormal, +} from '../../util/OverrideOpHelper.js' +import { LabelAndOverridesForMultiSelect } from '../../../../lib/Components/LabelAndOverrides' +import { + applyAndValidateOverrides, + ObjectWithOverrides, + SomeObjectOverrideOp, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { MultiSelectInputControl } from '../../../../lib/Components/MultiSelectInput' +import { useMemo } from 'react' +import { StudioPackageContainerIds } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' interface PackageContainersPickersProps { studio: DBStudio @@ -19,6 +30,47 @@ export function PackageContainersPickers({ }: PackageContainersPickersProps): JSX.Element { const { t } = useTranslation() + const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const prefixedOps = studio.packageContainerIdsWithOverrides.overrides.map((op) => ({ + ...op, + // TODO: can we avoid doing this hack? + path: `0.${op.path}`, + })) + + const computedValue = applyAndValidateOverrides(studio.packageContainerIdsWithOverrides).obj + + const wrappedItem: WrappedOverridableItemNormal = { + type: 'normal', + id: '0', + computed: computedValue, + defaults: studio.packageContainerIdsWithOverrides.defaults, + overrideOps: prefixedOps, + } + + const wrappedConfigObject: ObjectWithOverrides = { + defaults: studio.packageContainerIdsWithOverrides.defaults, + overrides: prefixedOps, + } + + return [wrappedItem, wrappedConfigObject] + }, [studio.packageContainerIdsWithOverrides]) + + const saveOverrides = React.useCallback( + (newOps: SomeObjectOverrideOp[]) => { + console.log('updating studio', newOps) + Studios.update(studio._id, { + $set: { + 'packageContainerIdsWithOverrides.overrides': newOps.map((op) => ({ + ...op, + path: op.path.startsWith('0.') ? op.path.slice(2) : op.path, + })), + }, + }) + }, + [studio._id] + ) + const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + const availablePackageContainerOptions = React.useMemo(() => { const arr: DropdownInputOption[] = [] @@ -45,32 +97,40 @@ export function PackageContainersPickers({ return (
-
- -
- + {(value, handleUpdate, options) => ( + -
-
-
- -
- + + {(value, handleUpdate, options) => ( + -
-
+ )} +
) }