diff --git a/DEVELOPER.md b/DEVELOPER.md index 140ac0712a..f4c0c9742d 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -142,8 +142,8 @@ However, one usage by AdlibActions for their userDataManifest remains as this is ## Blueprint Migrations -In R49, a replacement flow was added consisting of `validateConfig` and `applyConfig`. -It is no longer recommended to use the old migrations flow for showstyle and studio blueprints. +In R52, the replacement flow of `validateConfig` and `applyConfig` was extended to the system blueprint +It is no longer recommended to use the old migrations flow for system blueprints. ### ExpectedMediaItems diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 8f70712e48..5e4784ece2 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -105,7 +105,7 @@ export function defaultStudio(_id: StudioId): DBStudio { mappingsWithOverrides: wrapDefaultObject({}), supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, @@ -113,7 +113,7 @@ export function defaultStudio(_id: StudioId): DBStudio { allowHold: false, allowPieceDirectPlay: false, enableBuckets: false, - }, + }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 08c3653e21..ea62db5c5b 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -170,6 +170,25 @@ export async function setupMockCore(doc?: Partial): Promise { return { timelineObjects: [], @@ -424,7 +442,6 @@ export async function setupMockShowStyleBlueprint( }, showStyleConfigSchema: '{}' as any, - showStyleMigrations: [], getShowStyleVariantId: (): string | null => { return SHOW_STYLE_VARIANT_ID }, diff --git a/meteor/__mocks__/webapp.ts b/meteor/__mocks__/webapp.ts index 4e0b5a4a93..28bb8d218e 100644 --- a/meteor/__mocks__/webapp.ts +++ b/meteor/__mocks__/webapp.ts @@ -1,5 +1,5 @@ export const WebAppMock = { - rawConnectHandlers: { + rawHandlers: { use: (): void => { // No web server to setup }, diff --git a/meteor/server/__tests__/api/serviceMessages/serviceMessagesApi.test.ts b/meteor/server/__tests__/api/serviceMessages/serviceMessagesApi.test.ts index f210cd7286..ea9c79cc12 100644 --- a/meteor/server/__tests__/api/serviceMessages/serviceMessagesApi.test.ts +++ b/meteor/server/__tests__/api/serviceMessages/serviceMessagesApi.test.ts @@ -9,6 +9,7 @@ import { } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { CoreSystem } from '../../../collections' import { SupressLogMessages } from '../../../../__mocks__/suppressLogging' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' function convertExternalToServiceMessage(message: ExternalServiceMessage): ServiceMessage { return { @@ -42,6 +43,8 @@ const fakeCoreSystem: ICoreSystem = { version: '3', previousVersion: null, serviceMessages: {}, + settingsWithOverrides: wrapDefaultObject({} as any), + lastBlueprintConfig: undefined, } describe('Service messages internal API', () => { diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 104ed3de45..705587a606 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -2,7 +2,7 @@ import '../../__mocks__/_extendJest' import { runAllTimers, waitUntil } from '../../__mocks__/helpers/jest' import { MeteorMock } from '../../__mocks__/meteor' import { logger } from '../logging' -import { getRandomId, getRandomString, protectString } from '../lib/tempLib' +import { getRandomId, getRandomString, literal, protectString } from '../lib/tempLib' import { SnapshotType } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' import { IBlueprintPieceType, PieceLifespan, StatusCode, TSR } from '@sofie-automation/blueprints-integration' import { @@ -64,26 +64,36 @@ import { import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Settings } from '../Settings' import { SofieIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/SofieIngestDataCache' +import { ObjectOverrideSetOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' describe('cronjobs', () => { let env: DefaultEnvironment let rundownId: RundownId - beforeAll(async () => { - env = await setupDefaultStudioEnvironment() - - const o = await setupDefaultRundownPlaylist(env) - rundownId = o.rundownId - + async function setCasparCGCronEnabled(enabled: boolean) { await CoreSystem.updateAsync( {}, { - $set: { - 'cron.casparCGRestart.enabled': true, + // This is a little bit of a hack, as it will result in duplicate ops, but it's fine for unit tests + $push: { + 'settingsWithOverrides.overrides': literal({ + op: 'set', + path: 'cron.casparCGRestart.enabled', + value: enabled, + }), }, }, { multi: true } ) + } + + beforeAll(async () => { + env = await setupDefaultStudioEnvironment() + + const o = await setupDefaultRundownPlaylist(env) + rundownId = o.rundownId + + await setCasparCGCronEnabled(true) jest.useFakeTimers() // set time to 2020/07/19 00:00 Local Time @@ -597,15 +607,7 @@ describe('cronjobs', () => { }) test('Does not attempt to restart CasparCG when job is disabled', async () => { await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold - await CoreSystem.updateAsync( - {}, - { - $set: { - 'cron.casparCGRestart.enabled': false, - }, - }, - { multi: true } - ) + await setCasparCGCronEnabled(false) ;(logger.info as jest.Mock).mockClear() // set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, date++, 4, 5, 0).getTime() diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index b2c60d3d4a..af2cf6b8b1 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -55,8 +55,6 @@ describe('Test blueprint management api', () => { showStyleConfigSchema: JSONBlobStringify({}), databaseVersion: { - showStyle: {}, - studio: {}, system: undefined, }, @@ -238,7 +236,6 @@ describe('Test blueprint management api', () => { TSRVersion: '0.0.0', // studioConfigManifest: [], - // studioMigrations: [], // getBaseline: (context: IStudioContext): TSRTimelineObjBase[] => { // return [] // }, diff --git a/meteor/server/api/blueprints/__tests__/lib.ts b/meteor/server/api/blueprints/__tests__/lib.ts index 1b443d9715..e50bf8b59e 100644 --- a/meteor/server/api/blueprints/__tests__/lib.ts +++ b/meteor/server/api/blueprints/__tests__/lib.ts @@ -17,7 +17,6 @@ export function generateFakeBlueprint( integrationVersion: '0.0.0', TSRVersion: '0.0.0', studioConfigManifest: [], - studioMigrations: [], getBaseline: () => { return { timelineObjects: [], @@ -43,8 +42,6 @@ export function generateFakeBlueprint( showStyleConfigSchema: JSONBlobStringify({}), databaseVersion: { - showStyle: {}, - studio: {}, system: undefined, }, diff --git a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts index 419a13b9a7..2c44af3b17 100644 --- a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts +++ b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts @@ -16,1464 +16,6 @@ describe('Test blueprint migrationContext', () => { await setupDefaultStudioEnvironment() }) - // eslint-disable-next-line jest/no-commented-out-tests - /* - describe('MigrationContextStudio', () => { - async function getContext() { - const studio = (await Studios.findOneAsync({})) as DBStudio - expect(studio).toBeTruthy() - return new MigrationContextStudio(studio) - } - function getStudio(context: MigrationContextStudio): DBStudio { - const studio = (context as any).studio - expect(studio).toBeTruthy() - return studio - } - describe('mappings', () => { - async function getMappingFromDb(studio: DBStudio, mappingId: string): Promise { - const studio2 = (await Studios.findOneAsync(studio._id)) as DBStudio - expect(studio2).toBeTruthy() - return studio2.mappingsWithOverrides.defaults[mappingId] - } - - test('getMapping: no id', async () => { - const ctx = await getContext() - const mapping = ctx.getMapping('') - expect(mapping).toBeFalsy() - }) - test('getMapping: missing', async () => { - const ctx = await getContext() - const mapping = ctx.getMapping('fake_mapping') - expect(mapping).toBeFalsy() - }) - test('getMapping: good', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const rawMapping: MappingExt = { - device: TSR.DeviceType.ABSTRACT, - deviceId: protectString('dev1'), - lookahead: LookaheadMode.NONE, - options: {}, - } - studio.mappingsWithOverrides.defaults['mapping1'] = rawMapping - - const mapping = ctx.getMapping('mapping1') as BlueprintMapping - expect(mapping).toEqual(rawMapping) - - // Ensure it is a copy - mapping.deviceId = 'changed' - expect(mapping).not.toEqual(studio.mappingsWithOverrides.defaults['mapping1']) - }) - - test('insertMapping: good', async () => { - const ctx = await getContext() - - const rawMapping: BlueprintMapping = { - device: TSR.DeviceType.ABSTRACT, - deviceId: 'dev1', - lookahead: LookaheadMode.NONE, - options: {}, - } - - const mappingId = ctx.insertMapping('mapping2', rawMapping) - expect(mappingId).toEqual('mapping2') - - // get should return the same - const mapping = ctx.getMapping('mapping2') - expect(mapping).toEqual(rawMapping) - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') - expect(dbMapping).toEqual(rawMapping) - }) - test('insertMapping: no id', async () => { - const ctx = await getContext() - - const rawMapping: BlueprintMapping = { - device: TSR.DeviceType.ABSTRACT, - deviceId: 'dev1', - lookahead: LookaheadMode.NONE, - options: {}, - } - - expect(() => ctx.insertMapping('', rawMapping)).toThrow(`[500] Mapping id "" is invalid`) - - // get should return the same - const mapping = ctx.getMapping('') - expect(mapping).toBeFalsy() - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), '') - expect(dbMapping).toBeFalsy() - }) - test('insertMapping: existing', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('mapping2') - expect(existingMapping).toBeTruthy() - - const rawMapping: BlueprintMapping = { - device: TSR.DeviceType.ATEM, - deviceId: 'dev2', - lookahead: LookaheadMode.PRELOAD, - options: {}, - } - expect(rawMapping).not.toEqual(existingMapping) - - expect(() => ctx.insertMapping('mapping2', rawMapping)).toThrow( - `[404] Mapping "mapping2" cannot be inserted as it already exists` - ) - - // get should return the same - const mapping = ctx.getMapping('mapping2') - expect(mapping).toEqual(existingMapping) - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') - expect(dbMapping).toEqual(existingMapping) - }) - - test('updateMapping: good', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('mapping2') as BlueprintMapping - expect(existingMapping).toBeTruthy() - - const rawMapping = { - device: TSR.DeviceType.HYPERDECK, - deviceId: 'hyper0', - } - ctx.updateMapping('mapping2', rawMapping) - - const expectedMapping = { - ...existingMapping, - ...rawMapping, - } - - // get should return the same - const mapping = ctx.getMapping('mapping2') - expect(mapping).toEqual(expectedMapping) - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') - expect(dbMapping).toEqual(expectedMapping) - }) - test('updateMapping: no props', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('mapping2') as BlueprintMapping - expect(existingMapping).toBeTruthy() - - // Should not error - ctx.updateMapping('mapping2', {}) - }) - test('updateMapping: no id', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('') as BlueprintMapping - expect(existingMapping).toBeFalsy() - - expect(() => ctx.updateMapping('', { device: TSR.DeviceType.HYPERDECK })).toThrow( - `[404] Mapping "" cannot be updated as it does not exist` - ) - }) - test('updateMapping: missing', async () => { - const ctx = await getContext() - expect(ctx.getMapping('mapping1')).toBeFalsy() - - const rawMapping = { - device: TSR.DeviceType.HYPERDECK, - deviceId: 'hyper0', - } - - expect(() => ctx.updateMapping('mapping1', rawMapping)).toThrow( - `[404] Mapping "mapping1" cannot be updated as it does not exist` - ) - - // get should return the same - const mapping = ctx.getMapping('mapping1') - expect(mapping).toBeFalsy() - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping1') - expect(dbMapping).toBeFalsy() - }) - - test('removeMapping: missing', async () => { - const ctx = await getContext() - expect(ctx.getMapping('mapping1')).toBeFalsy() - - // Should not error - ctx.removeMapping('mapping1') - }) - test('removeMapping: no id', async () => { - const ctx = await getContext() - expect(ctx.getMapping('')).toBeFalsy() - expect(ctx.getMapping('mapping2')).toBeTruthy() - - // Should not error - ctx.removeMapping('') - - // ensure other mappings still exist - expect(await getMappingFromDb(getStudio(ctx), 'mapping2')).toBeTruthy() - }) - test('removeMapping: good', async () => { - const ctx = await getContext() - expect(ctx.getMapping('mapping2')).toBeTruthy() - - ctx.removeMapping('mapping2') - - // check was removed - expect(ctx.getMapping('mapping2')).toBeFalsy() - expect(await getMappingFromDb(getStudio(ctx), 'mapping2')).toBeFalsy() - }) - }) - - describe('config', () => { - async function getAllConfigFromDb(studio: DBStudio): Promise { - const studio2 = (await Studios.findOneAsync(studio._id)) as DBStudio - expect(studio2).toBeTruthy() - return studio2.blueprintConfigWithOverrides.defaults - } - - test('getConfig: no id', async () => { - const ctx = await getContext() - - expect(ctx.getConfig('')).toBeFalsy() - }) - test('getConfig: missing', async () => { - const ctx = await getContext() - - expect(ctx.getConfig('conf1')).toBeFalsy() - }) - test('getConfig: good', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - - studio.blueprintConfigWithOverrides.defaults['conf1'] = 5 - expect(ctx.getConfig('conf1')).toEqual(5) - - studio.blueprintConfigWithOverrides.defaults['conf2'] = ' af ' - expect(ctx.getConfig('conf2')).toEqual('af') - }) - - test('setConfig: no id', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - - expect(() => ctx.setConfig('', 34)).toThrow(`[500] Config id "" is invalid`) - - // Config should not have changed - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('setConfig: insert', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeFalsy() - - ctx.setConfig('conf1', 34) - - const expectedItem = { - _id: 'conf1', - value: 34, - } - expect(ctx.getConfig('conf1')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('setConfig: insert undefined', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('confUndef')).toBeFalsy() - - ctx.setConfig('confUndef', undefined as any) - - const expectedItem = { - _id: 'confUndef', - value: undefined as any, - } - expect(ctx.getConfig('confUndef')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - - test('setConfig: update', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - ctx.setConfig('conf1', 'hello') - - const expectedItem = { - _id: 'conf1', - value: 'hello', - } - expect(ctx.getConfig('conf1')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('setConfig: update undefined', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - ctx.setConfig('conf1', undefined as any) - - const expectedItem = { - _id: 'conf1', - value: undefined as any, - } - expect(ctx.getConfig('conf1')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - - test('removeConfig: no id', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - ctx.setConfig('conf1', true) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeConfig('') - - // Config should not have changed - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('removeConfig: missing', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - expect(ctx.getConfig('fake_conf')).toBeFalsy() - - // Should not error - ctx.removeConfig('fake_conf') - - // Config should not have changed - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('removeConfig: good', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeConfig('conf1') - - // Config should have changed - delete initialConfig['conf1'] - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - }) - - describe('devices', () => { - async function getStudio(context: MigrationContextStudio): Promise { - const studioId = (context as any).studio._id - const studio = (await Studios.findOneAsync(studioId)) as DBStudio - expect(studio).toBeTruthy() - return studio - } - async function createPlayoutDevice(studio: DBStudio) { - const peripheralDeviceId = getRandomId() - studio.peripheralDeviceSettings.playoutDevices.defaults = { - device01: { - peripheralDeviceId: peripheralDeviceId, - options: { - type: TSR.DeviceType.ABSTRACT, - options: {}, - }, - }, - } - - await Studios.updateAsync(studio._id, studio) - return PeripheralDevices.insertAsync({ - _id: peripheralDeviceId, - name: 'Fake parent device', - organizationId: null, - type: PeripheralDeviceType.PLAYOUT, - category: PeripheralDeviceCategory.PLAYOUT, - subType: PERIPHERAL_SUBTYPE_PROCESS, - deviceName: 'Playout Gateway', - studioId: studio._id, - created: 0, - lastConnected: 0, - lastSeen: 0, - status: { - statusCode: 0, - }, - connected: false, - connectionId: null, - token: '', - settings: {}, - configManifest: { - deviceConfigSchema: JSONBlobStringify({}), // can be empty as it's only useful for UI. - subdeviceManifest: {}, - }, - }) - } - async function getPlayoutDevice(studio: DBStudio): Promise { - const device = await PeripheralDevices.findOneAsync({ - studioId: studio._id, - type: PeripheralDeviceType.PLAYOUT, - category: PeripheralDeviceCategory.PLAYOUT, - subType: PERIPHERAL_SUBTYPE_PROCESS, - }) - expect(device).toBeTruthy() - return device as PeripheralDevice - } - - test('getDevice: no id', async () => { - const ctx = await getContext() - const device = ctx.getDevice('') - expect(device).toBeFalsy() - }) - test('getDevice: missing', async () => { - const ctx = await getContext() - const device = ctx.getDevice('fake_device') - expect(device).toBeFalsy() - }) - test('getDevice: missing with parent', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const playoutId = await createPlayoutDevice(studio) - expect(playoutId).toBeTruthy() - - const device = ctx.getDevice('fake_device') - expect(device).toBeFalsy() - }) - test('getDevice: good', async () => { - const ctx = await getContext() - const peripheral = getPlayoutDevice(await getStudio(ctx)) - expect(peripheral).toBeTruthy() - - const device = ctx.getDevice('device01') - expect(device).toBeTruthy() - - // Ensure bad id doesnt match it - const device2 = ctx.getDevice('fake_device') - expect(device2).toBeFalsy() - }) - - test('insertDevice: no id', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('')).toBeFalsy() - - expect(() => ctx.insertDevice('', { type: TSR.DeviceType.ABSTRACT } as any)).toThrow( - `[500] Device id "" is invalid` - ) - - expect(ctx.getDevice('')).toBeFalsy() - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('insertDevice: already exists', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device01')).toBeTruthy() - - expect(() => ctx.insertDevice('device01', { type: TSR.DeviceType.CASPARCG } as any)).toThrow( - `[404] Device "device01" cannot be inserted as it already exists` - ) - - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('insertDevice: ok', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device11')).toBeFalsy() - - const rawDevice: any = { type: TSR.DeviceType.CASPARCG } - - const deviceId = ctx.insertDevice('device11', rawDevice) - expect(deviceId).toEqual('device11') - initialSettings.defaults[deviceId] = { - peripheralDeviceId: (await getPlayoutDevice(studio))._id, - options: rawDevice, - } - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - - const device = ctx.getDevice(deviceId) - expect(device).toEqual(rawDevice) - }) - - test('updateDevice: no id', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('')).toBeFalsy() - - expect(() => ctx.updateDevice('', { type: TSR.DeviceType.ABSTRACT })).toThrow( - `[500] Device id "" is invalid` - ) - - expect(ctx.getDevice('')).toBeFalsy() - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('updateDevice: missing', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device22')).toBeFalsy() - - expect(() => ctx.updateDevice('device22', { type: TSR.DeviceType.ATEM })).toThrow( - `[404] Device "device22" cannot be updated as it does not exist` - ) - - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('Device: good', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device01')).toBeTruthy() - - const rawDevice: any = { - type: TSR.DeviceType.HYPERDECK, - } - const expectedDevice = { - ...initialSettings.defaults['device01'].options, - ...rawDevice, - } - - ctx.updateDevice('device01', rawDevice) - - expect(ctx.getDevice('device01')).toEqual(expectedDevice) - - initialSettings.defaults['device01'].options = expectedDevice - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - - test('removeDevice: no id', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('')).toBeFalsy() - - expect(() => ctx.removeDevice('')).toThrow(`[500] Device id "" is invalid`) - - expect(ctx.getDevice('')).toBeFalsy() - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('removeDevice: missing', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device22')).toBeFalsy() - - // Should not error - ctx.removeDevice('device22') - - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('removeDevice: good', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device01')).toBeTruthy() - - // Should not error - ctx.removeDevice('device01') - - expect(ctx.getDevice('device01')).toBeFalsy() - delete initialSettings.defaults['device01'] - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - }) - }) - - describe('MigrationContextShowStyle', () => { - async function getContext() { - const showStyle = (await ShowStyleBases.findOneAsync({})) as DBShowStyleBase - expect(showStyle).toBeTruthy() - return new MigrationContextShowStyle(showStyle) - } - function getShowStyle(context: MigrationContextShowStyle): DBShowStyleBase { - const showStyleBase = (context as any).showStyleBase - expect(showStyleBase).toBeTruthy() - return showStyleBase - } - async function createVariant(ctx: MigrationContextShowStyle, id: string, config?: IBlueprintConfig) { - const showStyle = getShowStyle(ctx) - - const rawVariant = literal({ - _id: protectString(ctx.getVariantId(id)), - name: 'test', - showStyleBaseId: showStyle._id, - blueprintConfigWithOverrides: wrapDefaultObject(config || {}), - _rundownVersionHash: '', - _rank: 0, - }) - await ShowStyleVariants.insertAsync(rawVariant) - - return rawVariant - } - - describe('variants', () => { - test('getAllVariants: good', async () => { - const ctx = await getContext() - const variants = ctx.getAllVariants() - expect(variants).toHaveLength(1) - }) - test('getAllVariants: missing base', () => { - const ctx = new MigrationContextShowStyle({ _id: 'fakeStyle' } as any) - const variants = ctx.getAllVariants() - expect(variants).toHaveLength(0) - }) - - test('getVariantId: consistent', async () => { - const ctx = await getContext() - - const id1 = ctx.getVariantId('variant1') - const id2 = ctx.getVariantId('variant1') - expect(id2).toEqual(id1) - - const id3 = ctx.getVariantId('variant2') - expect(id3).not.toEqual(id1) - }) - test('getVariantId: different base', async () => { - const ctx = await getContext() - const ctx2 = new MigrationContextShowStyle({ _id: 'fakeStyle' } as any) - - const id1 = ctx.getVariantId('variant1') - const id2 = ctx2.getVariantId('variant1') - expect(id2).not.toEqual(id1) - }) - - test('getVariant: good', async () => { - const ctx = await getContext() - const rawVariant = await createVariant(ctx, 'variant1') - - const variant = ctx.getVariant('variant1') - expect(variant).toBeTruthy() - expect(variant).toEqual(rawVariant) - }) - test('getVariant: no id', async () => { - const ctx = await getContext() - - expect(() => ctx.getVariant('')).toThrow(`[500] Variant id "" is invalid`) - }) - test('getVariant: missing', async () => { - const ctx = await getContext() - - const variant = ctx.getVariant('fake_variant') - expect(variant).toBeFalsy() - }) - - test('insertVariant: no id', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - - expect(() => - ctx.insertVariant('', { - name: 'test2', - }) - ).toThrow(`[500] Variant id "" is invalid`) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('insertVariant: already exists', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant1')).toBeTruthy() - - expect(() => - ctx.insertVariant('variant1', { - name: 'test2', - }) - ).toThrow(/*`[500] Variant id "variant1" already exists`* /) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('insertVariant: good', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant2')).toBeFalsy() - - const variantId = ctx.insertVariant('variant2', { - name: 'test2', - }) - expect(variantId).toBeTruthy() - expect(variantId).toEqual(ctx.getVariantId('variant2')) - - initialVariants.push( - literal({ - _id: protectString(variantId), - showStyleBaseId: getShowStyle(ctx)._id, - name: 'test2', - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - _rank: 0, - }) as any as IBlueprintShowStyleVariant - ) - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - - test('updateVariant: no id', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - - expect(() => - ctx.updateVariant('', { - name: 'test12', - }) - ).toThrow(`[500] Variant id "" is invalid`) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('updateVariant: missing', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant11')).toBeFalsy() - - expect(() => - ctx.updateVariant('variant11', { - name: 'test2', - }) - ).toThrow(/*`[404] Variant id "variant1" does not exist`* /) - // TODO - tidy up the error type - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('updateVariant: good', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant1')).toBeTruthy() - - ctx.updateVariant('variant1', { - name: 'newname', - }) - - _.each(initialVariants, (variant) => { - if (variant._id === ctx.getVariantId('variant1')) { - variant.name = 'newname' - } - }) - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - - test('removeVariant: no id', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - - expect(() => ctx.removeVariant('')).toThrow(`[500] Variant id "" is invalid`) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('removeVariant: missing', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant11')).toBeFalsy() - - // Should not error - ctx.removeVariant('variant11') - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('removeVariant: good', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant1')).toBeTruthy() - - // Should not error - ctx.removeVariant('variant1') - - const expectedVariants = _.filter( - initialVariants, - (variant) => variant._id !== ctx.getVariantId('variant1') - ) - expect(ctx.getAllVariants()).toEqual(expectedVariants) - }) - }) - - describe('sourcelayer', () => { - async function getAllSourceLayersFromDb(showStyle: DBShowStyleBase): Promise { - const showStyle2 = (await ShowStyleBases.findOneAsync(showStyle._id)) as DBShowStyleBase - expect(showStyle2).toBeTruthy() - return showStyle2.sourceLayersWithOverrides.defaults - } - - test('getSourceLayer: no id', async () => { - const ctx = await getContext() - - expect(() => ctx.getSourceLayer('')).toThrow(`[500] SourceLayer id "" is invalid`) - }) - test('getSourceLayer: missing', async () => { - const ctx = await getContext() - - const layer = ctx.getSourceLayer('fake_source_layer') - expect(layer).toBeFalsy() - }) - test('getSourceLayer: good', async () => { - const ctx = await getContext() - - const layer = ctx.getSourceLayer('cam0') as ISourceLayer - expect(layer).toBeTruthy() - expect(layer._id).toEqual('cam0') - - const layer2 = ctx.getSourceLayer('vt0') as ISourceLayer - expect(layer2).toBeTruthy() - expect(layer2._id).toEqual('vt0') - }) - - test('insertSourceLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.insertSourceLayer('', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[500] SourceLayer id "" is invalid`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('insertSourceLayer: existing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.insertSourceLayer('vt0', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[500] SourceLayer "vt0" already exists`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('insertSourceLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - const rawLayer = { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - } - - ctx.insertSourceLayer('lay1', rawLayer) - - initialSourceLayers['lay1'] = { - ...rawLayer, - _id: 'lay1', - } - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - - test('updateSourceLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.updateSourceLayer('', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[500] SourceLayer id "" is invalid`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('updateSourceLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.updateSourceLayer('fake99', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[404] SourceLayer "fake99" cannot be updated as it does not exist`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('updateSourceLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - expect(ctx.getSourceLayer('lay1')).toBeTruthy() - - const rawLayer = { - name: 'test98', - type: SourceLayerType.VT, - } - - ctx.updateSourceLayer('lay1', rawLayer) - - initialSourceLayers['lay1'] = { - ...initialSourceLayers['lay1']!, - ...rawLayer, - } - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - - test('removeSourceLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => ctx.removeSourceLayer('')).toThrow(`[500] SourceLayer id "" is invalid`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('removeSourceLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - expect(ctx.getSourceLayer('fake99')).toBeFalsy() - - // Should not error - ctx.removeSourceLayer('fake99') - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('removeSourceLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - expect(ctx.getSourceLayer('lay1')).toBeTruthy() - - // Should not error - ctx.removeSourceLayer('lay1') - - delete initialSourceLayers['lay1'] - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - }) - - describe('outputlayer', () => { - async function getAllOutputLayersFromDb( - showStyle: DBShowStyleBase - ): Promise> { - const showStyle2 = (await ShowStyleBases.findOneAsync(showStyle._id)) as DBShowStyleBase - expect(showStyle2).toBeTruthy() - return showStyle2.outputLayersWithOverrides.defaults - } - - test('getOutputLayer: no id', async () => { - const ctx = await getContext() - - expect(() => ctx.getOutputLayer('')).toThrow(`[500] OutputLayer id "" is invalid`) - }) - test('getOutputLayer: missing', async () => { - const ctx = await getContext() - - const layer = ctx.getOutputLayer('fake_source_layer') - expect(layer).toBeFalsy() - }) - test('getOutputLayer: good', async () => { - const ctx = await getContext() - - const layer = ctx.getOutputLayer('pgm') as IOutputLayer - expect(layer).toBeTruthy() - expect(layer._id).toEqual('pgm') - }) - - test('insertOutputLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.insertOutputLayer('', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[500] OutputLayer id "" is invalid`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('insertOutputLayer: existing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.insertOutputLayer('pgm', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[500] OutputLayer "pgm" already exists`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('insertOutputLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - const rawLayer = { - name: 'test', - _rank: 10, - isPGM: true, - } - - ctx.insertOutputLayer('lay1', rawLayer) - - initialOutputLayers['lay1'] = { - ...rawLayer, - _id: 'lay1', - } - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - - test('updateOutputLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.updateOutputLayer('', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[500] OutputLayer id "" is invalid`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('updateOutputLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.updateOutputLayer('fake99', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[404] OutputLayer "fake99" cannot be updated as it does not exist`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('updateOutputLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - expect(ctx.getOutputLayer('lay1')).toBeTruthy() - - const rawLayer = { - name: 'test98', - } - - ctx.updateOutputLayer('lay1', rawLayer) - - initialOutputLayers['lay1'] = { - ...initialOutputLayers['lay1']!, - ...rawLayer, - } - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - - test('removeOutputLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => ctx.removeOutputLayer('')).toThrow(`[500] OutputLayer id "" is invalid`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('removeOutputLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - expect(ctx.getOutputLayer('fake99')).toBeFalsy() - - // Should not error - ctx.removeOutputLayer('fake99') - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('removeOutputLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - expect(ctx.getOutputLayer('lay1')).toBeTruthy() - - // Should not error - ctx.removeOutputLayer('lay1') - - delete initialOutputLayers['lay1'] - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - }) - - describe('base-config', () => { - async function getAllBaseConfigFromDb(showStyle: DBShowStyleBase): Promise { - const showStyle2 = (await ShowStyleBases.findOneAsync(showStyle._id)) as DBShowStyleBase - expect(showStyle2).toBeTruthy() - return showStyle2.blueprintConfigWithOverrides.defaults - } - - test('getBaseConfig: no id', async () => { - const ctx = await getContext() - - expect(ctx.getBaseConfig('')).toBeFalsy() - }) - test('getBaseConfig: missing', async () => { - const ctx = await getContext() - - expect(ctx.getBaseConfig('conf1')).toBeFalsy() - }) - test('getBaseConfig: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - - showStyle.blueprintConfigWithOverrides.defaults['conf1'] = 5 - expect(ctx.getBaseConfig('conf1')).toEqual(5) - - showStyle.blueprintConfigWithOverrides.defaults['conf2'] = ' af ' - expect(ctx.getBaseConfig('conf2')).toEqual('af') - }) - - test('setBaseConfig: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - - expect(() => ctx.setBaseConfig('', 34)).toThrow(`[500] Config id "" is invalid`) - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('setBaseConfig: insert', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeFalsy() - - ctx.setBaseConfig('conf1', 34) - - const expectedItem = { - _id: 'conf1', - value: 34, - } - expect(ctx.getBaseConfig('conf1')).toEqual(expectedItem.value) - - // BaseConfig should have changed - initialBaseConfig[expectedItem._id] = expectedItem.value - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('setBaseConfig: insert undefined', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('confUndef')).toBeFalsy() - - expect(() => ctx.setBaseConfig('confUndef', undefined as any)).toThrow( - `[400] setBaseConfig "confUndef": value is undefined` - ) - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - - test('setBaseConfig: update', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - ctx.setBaseConfig('conf1', 'hello') - - const expectedItem = { - _id: 'conf1', - value: 'hello', - } - expect(ctx.getBaseConfig('conf1')).toEqual(expectedItem.value) - - // BaseConfig should have changed - initialBaseConfig[expectedItem._id] = expectedItem.value - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('setBaseConfig: update undefined', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - expect(() => ctx.setBaseConfig('conf1', undefined as any)).toThrow( - `[400] setBaseConfig "conf1": value is undefined` - ) - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - - test('removeBaseConfig: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - ctx.setBaseConfig('conf1', true) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeBaseConfig('') - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('removeBaseConfig: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - expect(ctx.getBaseConfig('fake_conf')).toBeFalsy() - - // Should not error - ctx.removeBaseConfig('fake_conf') - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('removeBaseConfig: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeBaseConfig('conf1') - - // BaseConfig should have changed - delete initialBaseConfig['conf1'] - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - }) - describe('variant-config', () => { - async function getAllVariantConfigFromDb( - ctx: MigrationContextShowStyle, - variantId: string - ): Promise { - const variant = (await ShowStyleVariants.findOneAsync( - protectString(ctx.getVariantId(variantId)) - )) as DBShowStyleVariant - expect(variant).toBeTruthy() - return variant.blueprintConfigWithOverrides.defaults - } - - test('getVariantConfig: no variant id', async () => { - const ctx = await getContext() - - expect(() => ctx.getVariantConfig('', 'conf1')).toThrow(`[404] ShowStyleVariant "" not found`) - }) - test('getVariantConfig: missing variant', async () => { - const ctx = await getContext() - - expect(() => ctx.getVariantConfig('fake_variant', 'conf1')).toThrow( - `[404] ShowStyleVariant "fake_variant" not found` - ) - }) - test('getVariantConfig: missing', async () => { - const ctx = await getContext() - await createVariant(ctx, 'configVariant', { conf1: 5, conf2: ' af ' }) - - expect(ctx.getVariantConfig('configVariant', 'conf11')).toBeFalsy() - }) - test('getVariantConfig: good', async () => { - const ctx = await getContext() - expect(ctx.getVariant('configVariant')).toBeTruthy() - - expect(ctx.getVariantConfig('configVariant', 'conf1')).toEqual(5) - expect(ctx.getVariantConfig('configVariant', 'conf2')).toEqual('af') - }) - - test('setVariantConfig: no variant id', async () => { - const ctx = await getContext() - - expect(() => ctx.setVariantConfig('', 'conf1', 5)).toThrow(`[404] ShowStyleVariant "" not found`) - }) - test('setVariantConfig: missing variant', async () => { - const ctx = await getContext() - - expect(() => ctx.setVariantConfig('fake_variant', 'conf1', 5)).toThrow( - `[404] ShowStyleVariant "fake_variant" not found` - ) - }) - test('setVariantConfig: no id', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariant('configVariant')).toBeTruthy() - - expect(() => ctx.setVariantConfig('configVariant', '', 34)).toThrow(`[500] Config id "" is invalid`) - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('setVariantConfig: insert', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf19')).toBeFalsy() - - ctx.setVariantConfig('configVariant', 'conf19', 34) - - const expectedItem = { - _id: 'conf19', - value: 34, - } - expect(ctx.getVariantConfig('configVariant', 'conf19')).toEqual(expectedItem.value) - - // VariantConfig should have changed - initialVariantConfig[expectedItem._id] = expectedItem.value - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('setVariantConfig: insert undefined', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'confUndef')).toBeFalsy() - - expect(() => ctx.setVariantConfig('configVariant', 'confUndef', undefined as any)).toThrow( - `[400] setVariantConfig "configVariant", "confUndef": value is undefined` - ) - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - - test('setVariantConfig: update', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - ctx.setVariantConfig('configVariant', 'conf1', 'hello') - - const expectedItem = { - _id: 'conf1', - value: 'hello', - } - expect(ctx.getVariantConfig('configVariant', 'conf1')).toEqual(expectedItem.value) - - // VariantConfig should have changed - initialVariantConfig[expectedItem._id] = expectedItem.value - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('setVariantConfig: update undefined', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - expect(() => ctx.setVariantConfig('configVariant', 'conf1', undefined as any)).toThrow( - `[400] setVariantConfig "configVariant", "conf1": value is undefined` - ) - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - - test('removeVariantConfig: no variant id', async () => { - const ctx = await getContext() - - expect(() => ctx.removeVariantConfig('', 'conf1')).toThrow(`[404] ShowStyleVariant "" not found`) - }) - test('removeVariantConfig: missing variant', async () => { - const ctx = await getContext() - - expect(() => ctx.removeVariantConfig('fake_variant', 'conf1')).toThrow( - `[404] ShowStyleVariant "fake_variant" not found` - ) - }) - test('removeVariantConfig: no id', async () => { - const ctx = await getContext() - ctx.setVariantConfig('configVariant', 'conf1', true) - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - // Should not error - ctx.removeVariantConfig('configVariant', '') - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('removeVariantConfig: missing', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - expect(ctx.getVariantConfig('configVariant', 'fake_conf')).toBeFalsy() - - // Should not error - ctx.removeVariantConfig('configVariant', 'fake_conf') - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('removeVariantConfig: good', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - // Should not error - ctx.removeVariantConfig('configVariant', 'conf1') - - // VariantConfig should have changed - delete initialVariantConfig['conf1'] - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - }) - }) - */ - describe('MigrationContextSystem', () => { async function getContext() { const coreSystem = await CoreSystem.findOneAsync({}) diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index 8a294bdfec..af378dd152 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -53,8 +53,6 @@ export async function insertBlueprint( blueprintType: type, databaseVersion: { - studio: {}, - showStyle: {}, system: undefined, }, @@ -154,8 +152,6 @@ async function innerUploadBlueprint( databaseVersion: blueprint ? blueprint.databaseVersion : { - studio: {}, - showStyle: {}, system: undefined, }, blueprintId: '', @@ -205,8 +201,6 @@ async function innerUploadBlueprint( // Force reset migrations newBlueprint.databaseVersion = { - showStyle: {}, - studio: {}, system: undefined, } } else { diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index a273f24bd1..c5ab3c15c1 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -11,11 +11,6 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { TriggeredActions } from '../../collections' -// function trimIfString(value: T): T | string { -// if (_.isString(value)) return value.trim() -// return value -// } - function convertTriggeredActionToBlueprints(triggeredAction: TriggeredActionsObj): IBlueprintTriggeredActions { const obj: Complete = { _id: unprotectString(triggeredAction._id), @@ -125,612 +120,3 @@ class AbstractMigrationContextWithTriggeredActions { export class MigrationContextSystem extends AbstractMigrationContextWithTriggeredActions implements IMigrationContextSystem {} - -/* -export class MigrationContextStudio implements IMigrationContextStudio { - private studio: DBStudio - - constructor(studio: DBStudio) { - this.studio = studio - } - - getMapping(mappingId: string): BlueprintMapping | undefined { - check(mappingId, String) - const mapping = this.studio.mappingsWithOverrides.defaults[mappingId] - if (mapping) { - return clone({ - ...mapping, - deviceId: unprotectString(mapping.deviceId), - }) - } - } - insertMapping(mappingId: string, mapping: OmitId): string { - check(mappingId, String) - if (this.studio.mappingsWithOverrides.defaults[mappingId]) { - throw new Meteor.Error(404, `Mapping "${mappingId}" cannot be inserted as it already exists`) - } - if (!mappingId) { - throw new Meteor.Error(500, `Mapping id "${mappingId}" is invalid`) - } - - const m: any = {} - m['mappingsWithOverrides.defaults.' + mappingId] = mapping - waitForPromise(Studios.updateAsync(this.studio._id, { $set: m })) - this.studio.mappingsWithOverrides.defaults[mappingId] = m['mappingsWithOverrides.defaults.' + mappingId] // Update local - return mappingId - } - updateMapping(mappingId: string, mapping: Partial): void { - check(mappingId, String) - if (!this.studio.mappingsWithOverrides.defaults[mappingId]) { - throw new Meteor.Error(404, `Mapping "${mappingId}" cannot be updated as it does not exist`) - } - - if (mappingId) { - const m: any = {} - m['mappingsWithOverrides.defaults.' + mappingId] = _.extend( - this.studio.mappingsWithOverrides.defaults[mappingId], - mapping - ) - waitForPromise(Studios.updateAsync(this.studio._id, { $set: m })) - this.studio.mappingsWithOverrides.defaults[mappingId] = m['mappingsWithOverrides.defaults.' + mappingId] // Update local - } - } - removeMapping(mappingId: string): void { - check(mappingId, String) - if (mappingId) { - const m: any = {} - m['mappingsWithOverrides.defaults.' + mappingId] = 1 - waitForPromise(Studios.updateAsync(this.studio._id, { $unset: m })) - delete this.studio.mappingsWithOverrides.defaults[mappingId] // Update local - } - } - - getConfig(configId: string): ConfigItemValue | undefined { - check(configId, String) - if (configId === '') return undefined - const configItem = objectPathGet(this.studio.blueprintConfigWithOverrides.defaults, configId) - return trimIfString(configItem) - } - setConfig(configId: string, value: ConfigItemValue): void { - check(configId, String) - if (!configId) { - throw new Meteor.Error(500, `Config id "${configId}" is invalid`) - } - - value = trimIfString(value) - - let modifier: MongoModifier = {} - if (value === undefined) { - modifier = { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - objectPathDelete(this.studio.blueprintConfigWithOverrides.defaults, configId) // Update local - } else { - modifier = { - $set: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: value, - }, - } - objectPathSet(this.studio.blueprintConfigWithOverrides.defaults, configId, value) // Update local - } - waitForPromise( - Studios.updateAsync( - { - _id: this.studio._id, - }, - modifier - ) - ) - } - removeConfig(configId: string): void { - check(configId, String) - - if (configId) { - waitForPromise( - Studios.updateAsync( - { - _id: this.studio._id, - }, - { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - ) - ) - // Update local: - objectPathDelete(this.studio.blueprintConfigWithOverrides.defaults, configId) - } - } - - getDevice(deviceId: string): TSR.DeviceOptionsAny | undefined { - check(deviceId, String) - - const studio = waitForPromise(Studios.findOneAsync(this.studio._id)) - if (!studio || !studio.peripheralDeviceSettings.playoutDevices) return undefined - - const playoutDevices = studio.peripheralDeviceSettings.playoutDevices.defaults - - return playoutDevices[deviceId]?.options - } - insertDevice(deviceId: string, device: TSR.DeviceOptionsAny): string { - check(deviceId, String) - - if (!deviceId) { - throw new Meteor.Error(500, `Device id "${deviceId}" is invalid`) - } - - const studio = waitForPromise(Studios.findOneAsync(this.studio._id)) - if (!studio || !studio.peripheralDeviceSettings.playoutDevices) - throw new Meteor.Error(500, `Studio was not found`) - - const playoutDevices = studio.peripheralDeviceSettings.playoutDevices.defaults - - if (playoutDevices && playoutDevices[deviceId]) { - throw new Meteor.Error(404, `Device "${deviceId}" cannot be inserted as it already exists`) - } - - const parentDevice = waitForPromise( - PeripheralDevices.findOneAsync( - { - type: PeripheralDeviceType.PLAYOUT, - subType: PERIPHERAL_SUBTYPE_PROCESS, - studioId: this.studio._id, - }, - { - sort: { - created: 1, - }, - } - ) - ) - if (!parentDevice) { - throw new Meteor.Error(404, `Device "${deviceId}" cannot be updated as it does not exist`) - } - - waitForPromise( - Studios.updateAsync(this.studio._id, { - $set: { - [`peripheralDeviceSettings.playoutDevices.defaults.${deviceId}`]: literal({ - peripheralDeviceId: parentDevice._id, - options: device, - }), - }, - }) - ) - - return deviceId - } - updateDevice(deviceId: string, device: Partial): void { - check(deviceId, String) - - if (!deviceId) { - throw new Meteor.Error(500, `Device id "${deviceId}" is invalid`) - } - - const studio = waitForPromise(Studios.findOneAsync(this.studio._id)) - if (!studio || !studio.peripheralDeviceSettings.playoutDevices) - throw new Meteor.Error(500, `Studio was not found`) - - const playoutDevices = studio.peripheralDeviceSettings.playoutDevices.defaults - - if (!playoutDevices || !playoutDevices[deviceId]) { - throw new Meteor.Error(404, `Device "${deviceId}" cannot be updated as it does not exist`) - } - - const newOptions = _.extend(playoutDevices[deviceId].options, device) - - waitForPromise( - Studios.updateAsync(this.studio._id, { - $set: { - [`peripheralDeviceSettings.playoutDevices.defaults.${deviceId}.options`]: newOptions, - }, - }) - ) - } - removeDevice(deviceId: string): void { - check(deviceId, String) - - if (!deviceId) { - throw new Meteor.Error(500, `Device id "${deviceId}" is invalid`) - } - - waitForPromise( - Studios.updateAsync(this.studio._id, { - $unset: { - [`peripheralDeviceSettings.playoutDevices.defaults.${deviceId}`]: 1, - }, - }) - ) - } -} - -export class MigrationContextShowStyle - extends AbstractMigrationContextWithTriggeredActions - implements IMigrationContextShowStyle -{ - private showStyleBase: DBShowStyleBase - constructor(showStyleBase: DBShowStyleBase) { - super() - this.showStyleBaseId = showStyleBase._id - this.showStyleBase = showStyleBase - } - - getAllVariants(): IBlueprintShowStyleVariant[] { - return waitForPromise( - ShowStyleVariants.findFetchAsync({ - showStyleBaseId: this.showStyleBase._id, - }) - ).map((variant) => unprotectObject(variant)) as any - } - getVariantId(variantId: string): string { - return getHash(this.showStyleBase._id + '_' + variantId) - } - private getProtectedVariantId(variantId: string): ShowStyleVariantId { - return protectString(this.getVariantId(variantId)) - } - private getVariantFromDb(variantId: string): DBShowStyleVariant | undefined { - const variant = waitForPromise( - ShowStyleVariants.findOneAsync({ - showStyleBaseId: this.showStyleBase._id, - _id: this.getProtectedVariantId(variantId), - }) - ) - if (variant) return variant - - // Assume we were given the full id - return waitForPromise( - ShowStyleVariants.findOneAsync({ - showStyleBaseId: this.showStyleBase._id, - _id: protectString(variantId), - }) - ) - } - getVariant(variantId: string): IBlueprintShowStyleVariant | undefined { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - - return unprotectObject(this.getVariantFromDb(variantId)) as any - } - insertVariant(variantId: string, variant: OmitId): string { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - - return unprotectString( - waitForPromise( - ShowStyleVariants.insertAsync({ - ...variant, - _id: this.getProtectedVariantId(variantId), - showStyleBaseId: this.showStyleBase._id, - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - _rank: 0, - }) - ) - ) - } - updateVariant(variantId: string, newVariant: Partial): void { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `Variant "${variantId}" not found`) - - waitForPromise(ShowStyleVariants.updateAsync(variant._id, { $set: newVariant })) - } - removeVariant(variantId: string): void { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - - waitForPromise( - ShowStyleVariants.removeAsync({ - _id: this.getProtectedVariantId(variantId), - showStyleBaseId: this.showStyleBase._id, - }) - ) - } - getSourceLayer(sourceLayerId: string): ISourceLayer | undefined { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - return this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - } - insertSourceLayer(sourceLayerId: string, layer: OmitId): string { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - if (oldLayer) { - throw new Meteor.Error(500, `SourceLayer "${sourceLayerId}" already exists`) - } - - const fullLayer: ISourceLayer = { - ...layer, - _id: sourceLayerId, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $set: { - [`sourceLayersWithOverrides.defaults.${sourceLayerId}`]: fullLayer, - }, - } - ) - ) - this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] = fullLayer // Update local - return fullLayer._id - } - updateSourceLayer(sourceLayerId: string, layer: Partial): void { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - if (!oldLayer) { - throw new Meteor.Error(404, `SourceLayer "${sourceLayerId}" cannot be updated as it does not exist`) - } - - const fullLayer = { - ...oldLayer, - ...layer, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - 'sourceLayers._id': sourceLayerId, - }, - { - $set: { - [`sourceLayersWithOverrides.defaults.${sourceLayerId}`]: fullLayer, - }, - }, - { multi: false } - ) - ) - this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] = fullLayer // Update local - } - removeSourceLayer(sourceLayerId: string): void { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $unset: { - [`sourceLayersWithOverrides.defaults.${sourceLayerId}`]: 1, - }, - } - ) - ) - // Update local: - delete this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - } - getOutputLayer(outputLayerId: string): IOutputLayer | undefined { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - return this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - } - insertOutputLayer(outputLayerId: string, layer: OmitId): string { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - if (oldLayer) { - throw new Meteor.Error(500, `OutputLayer "${outputLayerId}" already exists`) - } - - const fullLayer: IOutputLayer = { - ...layer, - _id: outputLayerId, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $set: { - [`outputLayersWithOverrides.defaults.${outputLayerId}`]: fullLayer, - }, - } - ) - ) - - this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] = fullLayer // Update local - return fullLayer._id - } - updateOutputLayer(outputLayerId: string, layer: Partial): void { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - if (!oldLayer) { - throw new Meteor.Error(404, `OutputLayer "${outputLayerId}" cannot be updated as it does not exist`) - } - - const fullLayer = { - ...oldLayer, - ...layer, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $set: { - [`outputLayersWithOverrides.defaults.${outputLayerId}`]: fullLayer, - }, - } - ) - ) - this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] = fullLayer // Update local - } - removeOutputLayer(outputLayerId: string): void { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $unset: { - [`outputLayersWithOverrides.defaults.${outputLayerId}`]: 1, - }, - } - ) - ) - // Update local: - delete this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - } - getBaseConfig(configId: string): ConfigItemValue | undefined { - check(configId, String) - if (configId === '') return undefined - const configItem = objectPathGet(this.showStyleBase.blueprintConfigWithOverrides.defaults, configId) - return trimIfString(configItem) - } - setBaseConfig(configId: string, value: ConfigItemValue): void { - check(configId, String) - if (!configId) { - throw new Meteor.Error(500, `Config id "${configId}" is invalid`) - } - - if (_.isUndefined(value)) throw new Meteor.Error(400, `setBaseConfig "${configId}": value is undefined`) - - value = trimIfString(value) - - const modifier: MongoModifier = { - $set: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: value, - }, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - modifier - ) - ) - objectPathSet(this.showStyleBase.blueprintConfigWithOverrides.defaults, configId, value) // Update local - } - removeBaseConfig(configId: string): void { - check(configId, String) - if (configId) { - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - ) - ) - // Update local: - objectPathDelete(this.showStyleBase.blueprintConfigWithOverrides.defaults, configId) - } - } - getVariantConfig(variantId: string, configId: string): ConfigItemValue | undefined { - check(variantId, String) - check(configId, String) - if (configId === '') return undefined - - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `ShowStyleVariant "${variantId}" not found`) - - const configItem = objectPathGet(variant.blueprintConfigWithOverrides.defaults, configId) - return trimIfString(configItem) - } - setVariantConfig(variantId: string, configId: string, value: ConfigItemValue): void { - check(variantId, String) - check(configId, String) - if (!configId) { - throw new Meteor.Error(500, `Config id "${configId}" is invalid`) - } - - value = trimIfString(value) - - if (_.isUndefined(value)) - throw new Meteor.Error(400, `setVariantConfig "${variantId}", "${configId}": value is undefined`) - - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `ShowStyleVariant "${variantId}" not found`) - - const modifier: MongoModifier = { - $set: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: value, - }, - } - waitForPromise( - ShowStyleVariants.updateAsync( - { - _id: variant._id, - }, - modifier - ) - ) - objectPathSet(variant.blueprintConfigWithOverrides.defaults, configId, value) // Update local - } - removeVariantConfig(variantId: string, configId: string): void { - check(variantId, String) - check(configId, String) - - if (configId) { - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `ShowStyleVariant "${variantId}" not found`) - - waitForPromise( - ShowStyleVariants.updateAsync( - { - _id: variant._id, - }, - { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - ) - ) - // Update local: - objectPathDelete(variant.blueprintConfigWithOverrides.defaults, configId) - } - } -} -*/ diff --git a/meteor/server/api/evaluations.ts b/meteor/server/api/evaluations.ts index 386acba33d..cfa30748db 100644 --- a/meteor/server/api/evaluations.ts +++ b/meteor/server/api/evaluations.ts @@ -9,6 +9,7 @@ import { fetchStudioLight } from '../optimizations' import { sendSlackMessageToWebhook } from './integration/slack' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Evaluations, RundownPlaylists } from '../collections' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { VerifiedRundownPlaylistForUserAction } from '../security/check' export async function saveEvaluation( @@ -30,8 +31,9 @@ export async function saveEvaluation( deferAsync(async () => { const studio = await fetchStudioLight(evaluation.studioId) if (!studio) throw new Meteor.Error(500, `Studio ${evaluation.studioId} not found!`) + const studioSettings = applyAndValidateOverrides(studio.settingsWithOverrides).obj - const webhookUrls = _.compact((studio.settings.slackEvaluationUrls || '').split(',')) + const webhookUrls = _.compact((studioSettings.slackEvaluationUrls || '').split(',')) if (webhookUrls.length) { // Only send notes if not everything is OK diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index 6fc91e8706..673e9c3174 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -46,13 +46,14 @@ Meteor.startup(() => { ) // Expose the API at the url - WebApp.rawConnectHandlers.use((req, res) => { + WebApp.rawHandlers.use((req, res) => { const transaction = profiler.startTransaction(`${req.method}:${req.url}`, 'http.incoming') if (transaction) { transaction.setLabel('url', `${req.url}`) transaction.setLabel('method', `${req.method}`) res.on('finish', () => { + // When the end of the request is sent to the client, submit the apm transaction let route = req.originalUrl if (req.originalUrl && req.url && req.originalUrl.endsWith(req.url.slice(1)) && req.url.length > 1) { route = req.originalUrl.slice(0, -1 * (req.url.length - 1)) diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index ff013539ba..e1ec97bea5 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -53,7 +53,7 @@ import { DEFAULT_FALLBACK_PART_DURATION, } from '@sofie-automation/shared-lib/dist/core/constants' import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' -import { ForceQuickLoopAutoNext } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /* This file contains functions that convert between the internal Sofie-Core types and types exposed to the external API. @@ -307,13 +307,17 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P : convertObjectIntoOverrides(await StudioBlueprintConfigFromAPI(apiStudio, blueprintManifest)) } + const studioSettings = studioSettingsFrom(apiStudio.settings) + return { _id: existingId ?? getRandomId(), name: apiStudio.name, blueprintId: blueprint?._id, blueprintConfigPresetId: apiStudio.blueprintConfigPresetId, blueprintConfigWithOverrides: blueprintConfig, - settings: studioSettingsFrom(apiStudio.settings), + settingsWithOverrides: studio + ? updateOverrides(studio.settingsWithOverrides, studioSettings) + : wrapDefaultObject(studioSettings), supportedShowStyleBase: apiStudio.supportedShowStyleBase?.map((id) => protectString(id)) ?? [], organizationId: null, mappingsWithOverrides: wrapDefaultObject({}), @@ -335,7 +339,7 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P } export async function APIStudioFrom(studio: DBStudio): Promise> { - const studioSettings = APIStudioSettingsFrom(studio.settings) + const studioSettings = APIStudioSettingsFrom(applyAndValidateOverrides(studio.settingsWithOverrides).obj) return { name: studio.name, diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 8cee7455cd..26ff61b1c5 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -47,14 +47,14 @@ export async function insertStudioInner(organizationId: OrganizationId | null, n supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), // testToolsConfig?: ITestToolsConfig - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: false, allowPieceDirectPlay: false, enableBuckets: true, - }, + }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), diff --git a/meteor/server/api/system.ts b/meteor/server/api/system.ts index 3c8bd9c4b8..11b90ab3eb 100644 --- a/meteor/server/api/system.ts +++ b/meteor/server/api/system.ts @@ -75,7 +75,7 @@ async function setupIndexes(removeOldIndexes = false): Promise { // Ensure indexes are created on startup: - ensureIndexes() + createIndexes() }) async function cleanupIndexes( diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index db05a469ee..52bb47eca6 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -18,15 +18,19 @@ import { profiler } from '../../api/profiler' import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types' import { AsyncOnlyMongoCollection } from '../collection' +/** + * A stripped down version of Meteor's Mongo.Cursor, with only the async methods + */ export type MinimalMongoCursor }> = Pick< MongoCursor, 'fetchAsync' | 'observeChangesAsync' | 'observeAsync' | 'countAsync' // | 'forEach' | 'map' | > - +/** + * A stripped down version of Meteor's Mongo.Collection, with only the async methods + */ export type MinimalMeteorMongoCollection }> = Pick< Mongo.Collection, - // | 'find' 'insertAsync' | 'removeAsync' | 'updateAsync' | 'upsertAsync' | 'rawCollection' | 'rawDatabase' | 'createIndex' > & { find: (...args: Parameters['find']>) => MinimalMongoCursor diff --git a/meteor/server/collections/index.ts b/meteor/server/collections/index.ts index d5d040dd95..9fbd43237c 100644 --- a/meteor/server/collections/index.ts +++ b/meteor/server/collections/index.ts @@ -54,14 +54,13 @@ export const CoreSystem = createAsyncOnlyMongoCollection(Collection if (!checkUserIdHasOneOfPermissions(userId, CollectionName.CoreSystem, 'configure')) return false return allowOnlyFields(doc, fields, [ - 'support', 'systemInfo', 'name', 'logLevel', 'apm', - 'cron', 'logo', - 'evaluations', + 'blueprintId', + 'settingsWithOverrides', ]) }, }) diff --git a/meteor/server/coreSystem/checkDatabaseVersions.ts b/meteor/server/coreSystem/checkDatabaseVersions.ts index 8fdd141b56..469b3e6914 100644 --- a/meteor/server/coreSystem/checkDatabaseVersions.ts +++ b/meteor/server/coreSystem/checkDatabaseVersions.ts @@ -1,8 +1,7 @@ import { StatusCode } from '@sofie-automation/blueprints-integration' -import { BlueprintId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { Blueprints, ShowStyleBases, Studios } from '../collections' +import { Blueprints } from '../collections' import { parseVersion, compareSemverVersions, @@ -10,8 +9,6 @@ import { isPrerelease, parseCoreIntegrationCompatabilityRange, } from '../systemStatus/semverUtils' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { lazyIgnore } from '../lib/lib' import { logger } from '../logging' import { CURRENT_SYSTEM_VERSION } from '../migration/currentSystemVersion' @@ -89,62 +86,7 @@ export function checkDatabaseVersions(): void { blueprintIds.add(blueprint._id) if (!blueprint.databaseVersion || typeof blueprint.databaseVersion === 'string') - blueprint.databaseVersion = { showStyle: {}, studio: {}, system: undefined } - if (!blueprint.databaseVersion.showStyle) blueprint.databaseVersion.showStyle = {} - if (!blueprint.databaseVersion.studio) blueprint.databaseVersion.studio = {} - - let o: { - statusCode: StatusCode - messages: string[] - } = { - statusCode: StatusCode.BAD, - messages: [], - } - - const checkedStudioIds = new Set() - - const showStylesForBlueprint = (await ShowStyleBases.findFetchAsync( - { blueprintId: blueprint._id }, - { - fields: { _id: 1 }, - } - )) as Array> - for (const showStyleBase of showStylesForBlueprint) { - if (o.statusCode === StatusCode.GOOD) { - o = compareSemverVersions( - parseVersion(blueprint.blueprintVersion), - parseRange(blueprint.databaseVersion.showStyle[unprotectString(showStyleBase._id)]), - false, - 'to fix, run migration', - 'blueprint version', - `showStyle "${showStyleBase._id}" migrations` - ) - } - - const studiosForShowStyleBase = (await Studios.findFetchAsync( - { supportedShowStyleBase: showStyleBase._id }, - { - fields: { _id: 1 }, - } - )) as Array> - for (const studio of studiosForShowStyleBase) { - if (!checkedStudioIds.has(studio._id)) { - // only run once per blueprint and studio - checkedStudioIds.add(studio._id) - - if (o.statusCode === StatusCode.GOOD) { - o = compareSemverVersions( - parseVersion(blueprint.blueprintVersion), - parseRange(blueprint.databaseVersion.studio[unprotectString(studio._id)]), - false, - 'to fix, run migration', - 'blueprint version', - `studio "${studio._id}]" migrations` - ) - } - } - } - } + blueprint.databaseVersion = { system: undefined } checkBlueprintCompability(blueprint) } diff --git a/meteor/server/coreSystem/index.ts b/meteor/server/coreSystem/index.ts index 95f4b74080..85a2586745 100644 --- a/meteor/server/coreSystem/index.ts +++ b/meteor/server/coreSystem/index.ts @@ -10,13 +10,14 @@ import { getEnvLogLevel, logger, LogLevel, setLogLevel } from '../logging' const PackageInfo = require('../../package.json') import { startAgent } from '../api/profiler/apm' import { profiler } from '../api/profiler' -import { TMP_TSR_VERSION } from '@sofie-automation/blueprints-integration' +import { ICoreSystemSettings, TMP_TSR_VERSION } from '@sofie-automation/blueprints-integration' import { getAbsolutePath } from '../lib' import * as fs from 'fs/promises' import path from 'path' import { checkDatabaseVersions } from './checkDatabaseVersions' import PLazy from 'p-lazy' import { getCoreSystemAsync } from './collection' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' export { PackageInfo } @@ -59,11 +60,25 @@ async function initializeCoreSystem() { enabled: false, transactionSampleRate: -1, }, - cron: { - casparCGRestart: { - enabled: true, + settingsWithOverrides: wrapDefaultObject({ + cron: { + casparCGRestart: { + enabled: true, + }, + storeRundownSnapshots: { + enabled: false, + }, }, - }, + support: { + message: '', + }, + evaluationsMessage: { + enabled: false, + heading: '', + message: '', + }, + }), + lastBlueprintConfig: undefined, }) if (!isRunningInJest()) { diff --git a/meteor/server/cronjobs.ts b/meteor/server/cronjobs.ts index 88bb16c851..7b9d0d1ffb 100644 --- a/meteor/server/cronjobs.ts +++ b/meteor/server/cronjobs.ts @@ -18,13 +18,14 @@ import { deferAsync, normalizeArrayToMap } from '@sofie-automation/corelib/dist/ import { getCoreSystemAsync } from './coreSystem/collection' import { cleanupOldDataInner } from './api/cleanup' import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system' -import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' import { executePeripheralDeviceFunctionWithCustomTimeout } from './api/peripheralDevice/executeFunction' import { interpollateTranslation, isTranslatableMessage, translateMessage, } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' const lowPrioFcn = (fcn: () => any) => { // Do it at a random time in the future: @@ -49,15 +50,17 @@ export async function nightlyCronjobInner(): Promise { logger.info('Nightly cronjob: starting...') const system = await getCoreSystemAsync() + const systemSettings = system && applyAndValidateOverrides(system.settingsWithOverrides).obj + await Promise.allSettled([ cleanupOldDataCronjob().catch((error) => { logger.error(`Cronjob: Error when cleaning up old data: ${stringifyError(error)}`) logger.error(error) }), - restartCasparCG(system, previousLastNightlyCronjob).catch((e) => { + restartCasparCG(systemSettings, previousLastNightlyCronjob).catch((e) => { logger.error(`Cron: Restart CasparCG error: ${stringifyError(e)}`) }), - storeSnapshots(system).catch((e) => { + storeSnapshots(systemSettings).catch((e) => { logger.error(`Cron: Rundown Snapshots error: ${stringifyError(e)}`) }), ]) @@ -81,8 +84,8 @@ async function cleanupOldDataCronjob() { const CASPARCG_LAST_SEEN_PERIOD_MS = 3 * 60 * 1000 // Note: this must be higher than the ping interval used by playout-gateway -async function restartCasparCG(system: ICoreSystem | undefined, previousLastNightlyCronjob: number) { - if (!system?.cron?.casparCGRestart?.enabled) return +async function restartCasparCG(systemSettings: ICoreSystemSettings | undefined, previousLastNightlyCronjob: number) { + if (!systemSettings?.cron?.casparCGRestart?.enabled) return let shouldRetryAttempt = false const ps: Array> = [] @@ -176,10 +179,10 @@ async function restartCasparCG(system: ICoreSystem | undefined, previousLastNigh } } -async function storeSnapshots(system: ICoreSystem | undefined) { - if (system?.cron?.storeRundownSnapshots?.enabled) { - const filter = system.cron.storeRundownSnapshots.rundownNames?.length - ? { name: { $in: system.cron.storeRundownSnapshots.rundownNames } } +async function storeSnapshots(systemSettings: ICoreSystemSettings | undefined) { + if (systemSettings?.cron?.storeRundownSnapshots?.enabled) { + const filter = systemSettings.cron.storeRundownSnapshots.rundownNames?.length + ? { name: { $in: systemSettings.cron.storeRundownSnapshots.rundownNames } } : {} const playlists = await RundownPlaylists.findFetchAsync(filter) diff --git a/meteor/server/logo.ts b/meteor/server/logo.ts index 26536e65b8..2e7910bae6 100644 --- a/meteor/server/logo.ts +++ b/meteor/server/logo.ts @@ -13,7 +13,7 @@ logoRouter.get('/', async (ctx) => { const logo = core?.logo ?? SofieLogo.Default const paths: Record = { - [SofieLogo.Default]: '/images/sofie-logo.svg', + [SofieLogo.Default]: '/images/sofie-logo-default.svg', [SofieLogo.Pride]: '/images/sofie-logo-pride.svg', [SofieLogo.Norway]: '/images/sofie-logo-norway.svg', [SofieLogo.Christmas]: '/images/sofie-logo-christmas.svg', diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index 81248aff75..5368274779 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -1,15 +1,9 @@ import { addMigrationSteps } from './databaseMigration' import { logger } from '../logging' -import { getRandomId, protectString, generateTranslation as t, getHash } from '../lib/tempLib' +import { getRandomId, protectString } from '../lib/tempLib' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ShowStyleBases, ShowStyleVariants, Studios, TriggeredActions } from '../collections' -import { - IBlueprintTriggeredActions, - ClientActions, - TriggerType, - PlayoutActions, -} from '@sofie-automation/blueprints-integration' +import { ShowStyleBases, ShowStyleVariants, Studios } from '../collections' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' /** @@ -17,408 +11,6 @@ import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/cor * These files are combined with / overridden by migration steps defined in the blueprints. */ -let j = 0 - -const DEFAULT_CORE_TRIGGERS: IBlueprintTriggeredActions[] = [ - { - _id: 'core_toggleShelf', - actions: { - '0': { - action: ClientActions.shelf, - filterChain: [ - { - object: 'view', - }, - ], - state: 'toggle', - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Tab', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Toggle Shelf'), - }, - { - _id: 'core_activateRundownPlaylist', - actions: { - '0': { - action: PlayoutActions.activateRundownPlaylist, - rehearsal: false, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Backquote', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Activate (On-Air)'), - }, - { - _id: 'core_activateRundownPlaylist_rehearsal', - actions: { - '0': { - action: PlayoutActions.activateRundownPlaylist, - rehearsal: true, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Backquote', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Activate (Rehearsal)'), - }, - { - _id: 'core_deactivateRundownPlaylist', - actions: { - '0': { - action: PlayoutActions.deactivateRundownPlaylist, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Shift+Backquote', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Deactivate'), - }, - { - _id: 'core_take', - actions: { - '0': { - action: PlayoutActions.take, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'NumpadEnter', - up: true, - }, - '1': { - type: TriggerType.hotkey, - keys: 'F12', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Take'), - }, - { - _id: 'core_hold', - actions: { - '0': { - action: PlayoutActions.hold, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'KeyH', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Hold'), - }, - { - _id: 'core_hold_undo', - actions: { - '0': { - action: PlayoutActions.hold, - undo: true, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+KeyH', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Undo Hold'), - }, - { - _id: 'core_reset_rundown_playlist', - actions: { - '0': { - action: PlayoutActions.resetRundownPlaylist, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Shift+F12', - up: true, - }, - '1': { - type: TriggerType.hotkey, - keys: 'Control+Shift+AnyEnter', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Reset Rundown'), - }, - { - _id: 'core_disable_next_piece', - actions: { - '0': { - action: PlayoutActions.disableNextPiece, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'KeyG', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Disable the next element'), - }, - { - _id: 'core_disable_next_piece_undo', - actions: { - '0': { - action: PlayoutActions.disableNextPiece, - filterChain: [ - { - object: 'view', - }, - ], - undo: true, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+KeyG', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Undo Disable the next element'), - }, - { - _id: 'core_create_snapshot_for_debug', - actions: { - '0': { - action: PlayoutActions.createSnapshotForDebug, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Backspace', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Store Snapshot'), - }, - { - _id: 'core_move_next_part', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: 1, - segments: 0, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'F9', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next forwards'), - }, - { - _id: 'core_move_next_segment', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: 0, - segments: 1, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'F10', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next to the following segment'), - }, - { - _id: 'core_move_previous_part', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: -1, - segments: 0, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+F9', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next backwards'), - }, - { - _id: 'core_move_previous_segment', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: 0, - segments: -1, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+F10', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next to the previous segment'), - }, - { - _id: 'core_go_to_onAir_line', - actions: { - '0': { - action: ClientActions.goToOnAirLine, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Home', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Go to On Air line'), - }, - { - _id: 'core_rewind_segments', - actions: { - '0': { - action: ClientActions.rewindSegments, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+Home', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Rewind segments to start'), - }, -] - // 0.1.0: These are the "base" migration steps, setting up a default system export const addSteps = addMigrationSteps('0.1.0', [ { @@ -437,14 +29,14 @@ export const addSteps = addMigrationSteps('0.1.0', [ name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: false, allowPieceDirectPlay: false, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', @@ -542,32 +134,4 @@ export const addSteps = addMigrationSteps('0.1.0', [ } }, }, - { - id: 'TriggeredActions.core', - canBeRunAutomatically: true, - validate: async () => { - const coreTriggeredActionsCount = await TriggeredActions.countDocuments({ - showStyleBaseId: null, - }) - - if (coreTriggeredActionsCount === 0) { - return `No system-wide triggered actions set up.` - } - - return false - }, - migrate: async () => { - for (const triggeredAction of DEFAULT_CORE_TRIGGERS) { - await TriggeredActions.insertAsync({ - _id: protectString(getHash(triggeredAction._id)), - _rank: triggeredAction._rank, - name: triggeredAction.name, - blueprintUniqueId: null, - showStyleBaseId: null, - actionsWithOverrides: wrapDefaultObject(triggeredAction.actions), - triggersWithOverrides: wrapDefaultObject(triggeredAction.triggers), - }) - } - }, - }, ]) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 41033e84ee..b4fd91c206 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,6 +1,6 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { PeripheralDevices, Studios } from '../collections' +import { CoreSystem, PeripheralDevices, Studios, TriggeredActions } from '../collections' import { convertObjectIntoOverrides, ObjectOverrideSetOp, @@ -10,8 +10,12 @@ import { StudioRouteSet, StudioRouteSetExclusivityGroup, StudioPackageContainer, + IStudioSettings, StudioDeviceSettings, } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DEFAULT_CORE_TRIGGER_IDS } from './upgrades/defaultSystemActionTriggers' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' import { logger } from '../logging' import { literal, unprotectString } from '../lib/tempLib' @@ -232,6 +236,123 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ }, }, + { + id: 'TriggeredActions.remove old systemwide', + canBeRunAutomatically: true, + validate: async () => { + const coreTriggeredActionsCount = await TriggeredActions.countDocuments({ + showStyleBaseId: null, + blueprintUniqueId: null, + _id: { $in: DEFAULT_CORE_TRIGGER_IDS }, + }) + + if (coreTriggeredActionsCount > 0) { + return `System-wide triggered actions needing removal.` + } + + return false + }, + migrate: async () => { + await TriggeredActions.removeAsync({ + showStyleBaseId: null, + blueprintUniqueId: null, + _id: { $in: DEFAULT_CORE_TRIGGER_IDS }, + }) + }, + }, + + { + id: `convert studio.settings to ObjectWithOverrides`, + canBeRunAutomatically: true, + validate: async () => { + const studios = await Studios.findFetchAsync({ + settings: { $exists: true }, + settingsWithOverrides: { $exists: false }, + }) + + for (const studio of studios) { + //@ts-expect-error settings is not typed as ObjectWithOverrides + if (studio.settings) { + return 'settings must be converted to an ObjectWithOverrides' + } + } + + return false + }, + migrate: async () => { + const studios = await Studios.findFetchAsync({ + settings: { $exists: true }, + settingsWithOverrides: { $exists: false }, + }) + + for (const studio of studios) { + //@ts-expect-error settings is typed as Record + const oldSettings = studio.settings + + const newSettings = wrapDefaultObject(oldSettings || {}) + + await Studios.updateAsync(studio._id, { + $set: { + settingsWithOverrides: newSettings, + }, + $unset: { + // settings: 1, + }, + }) + } + }, + }, + + { + id: `convert CoreSystem.settingsWithOverrides`, + canBeRunAutomatically: true, + validate: async () => { + const systems = await CoreSystem.findFetchAsync({ + settingsWithOverrides: { $exists: false }, + }) + + if (systems.length > 0) { + return 'settings must be converted to an ObjectWithOverrides' + } + + return false + }, + migrate: async () => { + const systems = await CoreSystem.findFetchAsync({ + settingsWithOverrides: { $exists: false }, + }) + + for (const system of systems) { + const oldSystem = system as ICoreSystem as PartialOldICoreSystem + + const newSettings = wrapDefaultObject({ + cron: { + casparCGRestart: { + enabled: false, + }, + storeRundownSnapshots: { + enabled: false, + }, + ...oldSystem.cron, + }, + support: oldSystem.support ?? { message: '' }, + evaluationsMessage: oldSystem.evaluations ?? { enabled: false, heading: '', message: '' }, + }) + + await CoreSystem.updateAsync(system._id, { + $set: { + settingsWithOverrides: newSettings, + }, + $unset: { + cron: 1, + support: 1, + evaluations: 1, + }, + }) + } + }, + }, + { id: `studios create peripheralDeviceSettings.deviceSettings`, canBeRunAutomatically: true, @@ -370,3 +491,27 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ }, }, ]) + +interface PartialOldICoreSystem { + /** Support info */ + support?: { + message: string + } + + evaluations?: { + enabled: boolean + heading: string + message: string + } + + /** Cron jobs running nightly */ + cron?: { + casparCGRestart?: { + enabled: boolean + } + storeRundownSnapshots?: { + enabled: boolean + rundownNames?: string[] + } + } +} diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index a230bc8077..973610f322 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -5,21 +5,11 @@ import { clearMigrationSteps, addMigrationSteps, prepareMigration, PreparedMigra import { CURRENT_SYSTEM_VERSION } from '../currentSystemVersion' import { RunMigrationResult, GetMigrationStatusResult } from '@sofie-automation/meteor-lib/dist/api/migration' import { literal, protectString } from '../../lib/tempLib' -import { - MigrationStepInputResult, - BlueprintManifestType, - MigrationContextStudio, - MigrationContextShowStyle, - PlaylistTimingType, - PlaylistTimingNone, - ShowStyleBlueprintManifest, - StudioBlueprintManifest, -} from '@sofie-automation/blueprints-integration' +import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { generateFakeBlueprint } from '../../api/blueprints/__tests__/lib' import { MeteorCall } from '../../api/methods' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { Blueprints, ShowStyleBases, ShowStyleVariants, Studios } from '../../collections' +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' @@ -121,14 +111,14 @@ describe('Migrations', () => { name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', @@ -163,14 +153,14 @@ describe('Migrations', () => { name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', @@ -205,14 +195,14 @@ describe('Migrations', () => { name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', @@ -250,160 +240,6 @@ describe('Migrations', () => { const studio = (await Studios.findOneAsync({})) as DBStudio expect(studio).toBeTruthy() - const studioManifest = (): StudioBlueprintManifest => ({ - blueprintType: 'studio' as BlueprintManifestType.STUDIO, - blueprintVersion: '1.0.0', - integrationVersion: '0.0.0', - TSRVersion: '0.0.0', - - configPresets: { - main: { - name: 'Main', - config: {}, - }, - }, - - studioConfigSchema: '{}' as any, - studioMigrations: [ - { - version: '0.2.0', - id: 'myStudioMockStep2', - validate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest2')) return `mocktest2 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest2')) { - context.setConfig('mocktest2', true) - } - }, - }, - { - version: '0.3.0', - id: 'myStudioMockStep3', - validate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest3')) return `mocktest3 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest3')) { - context.setConfig('mocktest3', true) - } - }, - }, - { - version: '0.1.0', - id: 'myStudioMockStep1', - validate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest1')) return `mocktest1 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest1')) { - context.setConfig('mocktest1', true) - } - }, - }, - ], - getBaseline: () => { - return { - timelineObjects: [], - } - }, - getShowStyleId: () => null, - }) - - const showStyleManifest = (): ShowStyleBlueprintManifest => ({ - blueprintType: 'showstyle' as BlueprintManifestType.SHOWSTYLE, - blueprintVersion: '1.0.0', - integrationVersion: '0.0.0', - TSRVersion: '0.0.0', - - configPresets: { - main: { - name: 'Main', - config: {}, - - variants: { - main: { - name: 'Default', - config: {}, - }, - }, - }, - }, - - showStyleConfigSchema: '{}' as any, - showStyleMigrations: [ - { - version: '0.2.0', - id: 'myShowStyleMockStep2', - validate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest2')) return `mocktest2 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest2')) { - context.setBaseConfig('mocktest2', true) - } - }, - }, - { - version: '0.3.0', - id: 'myShowStyleMockStep3', - validate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest3')) return `mocktest3 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest3')) { - context.setBaseConfig('mocktest3', true) - } - }, - }, - { - version: '0.1.0', - id: 'myShowStyleMockStep1', - validate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest1')) return `mocktest1 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest1')) { - context.setBaseConfig('mocktest1', true) - } - }, - }, - ], - getShowStyleVariantId: () => null, - getRundown: () => ({ - rundown: { - externalId: '', - name: '', - timing: literal({ - type: PlaylistTimingType.None, - }), - }, - globalAdLibPieces: [], - globalActions: [], - baseline: { timelineObjects: [] }, - }), - getSegment: () => ({ - segment: { name: '' }, - parts: [], - }), - }) - - await Blueprints.insertAsync( - generateFakeBlueprint('showStyle0', BlueprintManifestType.SHOWSTYLE, showStyleManifest) - ) - await ShowStyleBases.insertAsync({ _id: protectString('showStyle0'), name: '', @@ -427,7 +263,6 @@ describe('Migrations', () => { _rank: 0, }) - await Blueprints.insertAsync(generateFakeBlueprint('studio0', BlueprintManifestType.STUDIO, studioManifest)) await Studios.updateAsync(studio._id, { $set: { blueprintId: protectString('studio0'), diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index 16d6cab3ab..0557480278 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -19,8 +19,9 @@ import { validateConfigForShowStyleBase, validateConfigForStudio, } from './upgrades' -import { ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { CoreSystemId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' +import { runUpgradeForCoreSystem } from './upgrades/system' import { assertConnectionHasOneOfPermissions } from '../security/auth' import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' @@ -128,5 +129,13 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return runUpgradeForShowStyleBase(showStyleBaseId) } + + async runUpgradeForCoreSystem(coreSystemId: CoreSystemId): Promise { + check(coreSystemId, String) + + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) + + return runUpgradeForCoreSystem(coreSystemId) + } } registerClassToMeteorMethods(MigrationAPIMethods, ServerMigrationAPI, false) diff --git a/meteor/server/migration/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index 42b0d76b1e..4075e8ae2b 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -4,25 +4,15 @@ import { BlueprintManifestType, InputFunctionCore, InputFunctionSystem, - InputFunctionShowStyle, - InputFunctionStudio, MigrateFunctionCore, - MigrateFunctionShowStyle, - MigrateFunctionStudio, MigrationContextSystem as IMigrationContextSystem, - MigrationContextShowStyle as IMigrationContextShowStyle, - MigrationContextStudio as IMigrationContextStudio, MigrationStep, MigrationStepInput, MigrationStepInputFilteredResult, MigrationStepInputResult, - ShowStyleBlueprintManifest, - StudioBlueprintManifest, SystemBlueprintManifest, ValidateFunctionCore, ValidateFunctionSystem, - ValidateFunctionShowStyle, - ValidateFunctionStudio, MigrateFunctionSystem, ValidateFunction, MigrateFunction, @@ -40,13 +30,13 @@ import { logger } from '../logging' import { internalStoreSystemSnapshot } from '../api/snapshot' import { parseVersion, Version } from '../systemStatus/semverUtils' import { GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { clone, getHash, omit, protectString, unprotectString } from '../lib/tempLib' +import { clone, getHash, omit, protectString } from '../lib/tempLib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { evalBlueprint } from '../api/blueprints/cache' import { MigrationContextSystem } from '../api/blueprints/migrationContext' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { SnapshotId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Blueprints, CoreSystem, ShowStyleBases, Studios } from '../collections' +import { SnapshotId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Blueprints, CoreSystem } from '../collections' import { getSystemStorePath } from '../coreSystem' import { getCoreSystemAsync, setCoreSystemVersion } from '../coreSystem/collection' @@ -158,102 +148,24 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise { - const chunk: MigrationChunk = { - sourceType: MigrationStepType.SHOWSTYLE, - sourceName: 'Blueprint ' + blueprint.name + ' for showStyle ' + showStyleBase.name, - blueprintId: blueprint._id, - sourceId: showStyleBase._id, - _dbVersion: parseVersion( - blueprint.databaseVersion.showStyle[unprotectString(showStyleBase._id)] || '0.0.0' - ), - _targetVersion: parseVersion(bp.blueprintVersion), - _steps: [], - } - migrationChunks.push(chunk) - // Add show-style migration steps from blueprint: - for (const step of bp.showStyleMigrations) { - allMigrationSteps.push( - prefixIdsOnStep('blueprint_' + blueprint._id + '_showStyle_' + showStyleBase._id + '_', { - id: step.id, - overrideSteps: step.overrideSteps, - validate: step.validate, - canBeRunAutomatically: step.canBeRunAutomatically, - migrate: step.migrate, - input: step.input, - dependOnResultFrom: step.dependOnResultFrom, - version: step.version, - _version: parseVersion(step.version), - _validateResult: false, // to be set later - _rank: rank++, - chunk: chunk, - }) - ) - } - }) - } else if (blueprint.blueprintType === BlueprintManifestType.STUDIO) { - const bp = blueprintManifest as StudioBlueprintManifest + if (blueprint.blueprintType === BlueprintManifestType.SYSTEM) { + const bp = blueprintManifest as SystemBlueprintManifest // If blueprint uses the new flow, don't attempt migrations if (typeof bp.applyConfig === 'function') continue - // Find all studios that use this blueprint - const studios = await Studios.findFetchAsync({ blueprintId: blueprint._id }) - studios.forEach((studio) => { - const chunk: MigrationChunk = { - sourceType: MigrationStepType.STUDIO, - sourceName: 'Blueprint ' + blueprint.name + ' for studio ' + studio.name, - blueprintId: blueprint._id, - sourceId: studio._id, - _dbVersion: parseVersion( - blueprint.databaseVersion.studio[unprotectString(studio._id)] || '0.0.0' - ), - _targetVersion: parseVersion(bp.blueprintVersion), - _steps: [], - } - migrationChunks.push(chunk) - // Add studio migration steps from blueprint: - for (const step of bp.studioMigrations) { - allMigrationSteps.push( - prefixIdsOnStep('blueprint_' + blueprint._id + '_studio_' + studio._id + '_', { - id: step.id, - overrideSteps: step.overrideSteps, - validate: step.validate, - canBeRunAutomatically: step.canBeRunAutomatically, - migrate: step.migrate, - input: step.input, - dependOnResultFrom: step.dependOnResultFrom, - version: step.version, - _version: parseVersion(step.version), - _validateResult: false, // to be set later - _rank: rank++, - chunk: chunk, - }) - ) - } - }) - } else if (blueprint.blueprintType === BlueprintManifestType.SYSTEM) { - const bp = blueprintManifest as SystemBlueprintManifest // Check if the coreSystem uses this blueprint const coreSystems = await CoreSystem.findFetchAsync({ blueprintId: blueprint._id, @@ -307,15 +219,6 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise) { for (const chunk of chunks) { if (chunk.sourceType === MigrationStepType.CORE) { await setCoreSystemVersion(chunk._targetVersion) - } else if ( - chunk.sourceType === MigrationStepType.STUDIO || - chunk.sourceType === MigrationStepType.SHOWSTYLE || - chunk.sourceType === MigrationStepType.SYSTEM - ) { + } else if (chunk.sourceType === MigrationStepType.SYSTEM) { if (!chunk.blueprintId) throw new Meteor.Error(500, `chunk.blueprintId missing!`) if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing!`) @@ -708,20 +583,6 @@ async function completeMigration(chunks: Array) { `Updating Blueprint "${chunk.sourceName}" version, from "${blueprint.databaseVersion.system}" to "${chunk._targetVersion}".` ) m[`databaseVersion.system`] = chunk._targetVersion - } else if (chunk.sourceType === MigrationStepType.STUDIO && chunk.sourceId !== 'system') { - logger.info( - `Updating Blueprint "${chunk.sourceName}" version, from "${ - blueprint.databaseVersion.studio[unprotectString(chunk.sourceId)] - }" to "${chunk._targetVersion}".` - ) - m[`databaseVersion.studio.${chunk.sourceId}`] = chunk._targetVersion - } else if (chunk.sourceType === MigrationStepType.SHOWSTYLE && chunk.sourceId !== 'system') { - logger.info( - `Updating Blueprint "${chunk.sourceName}" version, from "${ - blueprint.databaseVersion.showStyle[unprotectString(chunk.sourceId)] - }" to "${chunk._targetVersion}".` - ) - m[`databaseVersion.showStyle.${chunk.sourceId}`] = chunk._targetVersion } else throw new Meteor.Error(500, `Bad chunk.sourcetype: "${chunk.sourceType}"`) await Blueprints.updateAsync(chunk.blueprintId, { $set: m }) @@ -777,8 +638,6 @@ export async function resetDatabaseVersions(): Promise { { $set: { databaseVersion: { - studio: {}, - showStyle: {}, system: '', }, }, @@ -794,29 +653,3 @@ function getMigrationSystemContext(chunk: MigrationChunk): IMigrationContextSyst return new MigrationContextSystem() } -async function getMigrationStudioContext(chunk: MigrationChunk): Promise { - if (chunk.sourceType !== MigrationStepType.STUDIO) - throw new Meteor.Error(500, `wrong chunk.sourceType "${chunk.sourceType}", expected STUDIO`) - if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing`) - if (chunk.sourceId === 'system') - throw new Meteor.Error(500, `cunk.sourceId invalid in this context: ${chunk.sourceId}`) - - const studio = await Studios.findOneAsync(chunk.sourceId as StudioId) - if (!studio) throw new Meteor.Error(404, `Studio "${chunk.sourceId}" not found`) - - // return new MigrationContextStudio(studio) - throw new Meteor.Error(500, 'Studio migrations not supported!') -} -async function getMigrationShowStyleContext(chunk: MigrationChunk): Promise { - if (chunk.sourceType !== MigrationStepType.SHOWSTYLE) - throw new Meteor.Error(500, `wrong chunk.sourceType "${chunk.sourceType}", expected SHOWSTYLE`) - if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing`) - if (chunk.sourceId === 'system') - throw new Meteor.Error(500, `cunk.sourceId invalid in this context: ${chunk.sourceId}`) - - const showStyleBase = await ShowStyleBases.findOneAsync(chunk.sourceId as ShowStyleBaseId) - if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase "${chunk.sourceId}" not found`) - - // return new MigrationContextShowStyle(showStyleBase) - throw new Meteor.Error(500, 'ShowStyle migrations not supported!') -} diff --git a/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts b/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts index 645f78e989..9ac3aff7ca 100644 --- a/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts +++ b/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts @@ -49,7 +49,6 @@ describe('ShowStyleBase upgrades', () => { }, showStyleConfigSchema: JSONBlobStringify({}), - showStyleMigrations: [], getShowStyleVariantId: (): string | null => { return null }, diff --git a/meteor/server/migration/upgrades/context.ts b/meteor/server/migration/upgrades/context.ts index 5e0f7b6af1..921032dd8b 100644 --- a/meteor/server/migration/upgrades/context.ts +++ b/meteor/server/migration/upgrades/context.ts @@ -1,6 +1,12 @@ -import { ICommonContext, NoteSeverity } from '@sofie-automation/blueprints-integration' -import { assertNever, getHash } from '@sofie-automation/corelib/dist/lib' +import { + IBlueprintDefaultCoreSystemTriggers, + ICommonContext, + NoteSeverity, +} from '@sofie-automation/blueprints-integration' +import { assertNever, clone, getHash } from '@sofie-automation/corelib/dist/lib' import { logger } from '../../logging' +import { ICoreSystemApplyConfigContext } from '@sofie-automation/blueprints-integration/dist/context/systemApplyConfigContext' +import { DEFAULT_CORE_TRIGGERS } from './defaultSystemActionTriggers' /** * This is almost identical to the one in the job-worker, but it is hard to share the implementation due to differing loggers @@ -56,3 +62,9 @@ export class CommonContext implements ICommonContext { } } } + +export class CoreSystemApplyConfigContext extends CommonContext implements ICoreSystemApplyConfigContext { + getDefaultSystemActionTriggers(): IBlueprintDefaultCoreSystemTriggers { + return clone(DEFAULT_CORE_TRIGGERS) + } +} diff --git a/meteor/server/migration/upgrades/defaultSystemActionTriggers.ts b/meteor/server/migration/upgrades/defaultSystemActionTriggers.ts new file mode 100644 index 0000000000..29242cbecd --- /dev/null +++ b/meteor/server/migration/upgrades/defaultSystemActionTriggers.ts @@ -0,0 +1,416 @@ +import { + IBlueprintTriggeredActions, + ClientActions, + TriggerType, + PlayoutActions, + IBlueprintDefaultCoreSystemTriggersType, + IBlueprintDefaultCoreSystemTriggers, +} from '@sofie-automation/blueprints-integration' +import { getHash, protectString, generateTranslation as t } from '../../lib/tempLib' +import { TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +let j = 0 + +export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = { + [IBlueprintDefaultCoreSystemTriggersType.toggleShelf]: { + _id: 'core_toggleShelf', + actions: { + '0': { + action: ClientActions.shelf, + filterChain: [ + { + object: 'view', + }, + ], + state: 'toggle', + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Tab', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Toggle Shelf'), + }, + [IBlueprintDefaultCoreSystemTriggersType.activateRundownPlaylist]: { + _id: 'core_activateRundownPlaylist', + actions: { + '0': { + action: PlayoutActions.activateRundownPlaylist, + rehearsal: false, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Backquote', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Activate (On-Air)'), + }, + [IBlueprintDefaultCoreSystemTriggersType.activateRundownPlaylistRehearsal]: { + _id: 'core_activateRundownPlaylist_rehearsal', + actions: { + '0': { + action: PlayoutActions.activateRundownPlaylist, + rehearsal: true, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Control+Backquote', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Activate (Rehearsal)'), + }, + [IBlueprintDefaultCoreSystemTriggersType.deactivateRundownPlaylist]: { + _id: 'core_deactivateRundownPlaylist', + actions: { + '0': { + action: PlayoutActions.deactivateRundownPlaylist, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Control+Shift+Backquote', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Deactivate'), + }, + [IBlueprintDefaultCoreSystemTriggersType.take]: { + _id: 'core_take', + actions: { + '0': { + action: PlayoutActions.take, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'NumpadEnter', + up: true, + }, + '1': { + type: TriggerType.hotkey, + keys: 'F12', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Take'), + }, + [IBlueprintDefaultCoreSystemTriggersType.hold]: { + _id: 'core_hold', + actions: { + '0': { + action: PlayoutActions.hold, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'KeyH', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Hold'), + }, + [IBlueprintDefaultCoreSystemTriggersType.holdUndo]: { + _id: 'core_hold_undo', + actions: { + '0': { + action: PlayoutActions.hold, + undo: true, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Shift+KeyH', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Undo Hold'), + }, + [IBlueprintDefaultCoreSystemTriggersType.resetRundownPlaylist]: { + _id: 'core_reset_rundown_playlist', + actions: { + '0': { + action: PlayoutActions.resetRundownPlaylist, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Control+Shift+F12', + up: true, + }, + '1': { + type: TriggerType.hotkey, + keys: 'Control+Shift+AnyEnter', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Reset Rundown'), + }, + [IBlueprintDefaultCoreSystemTriggersType.disableNextPiece]: { + _id: 'core_disable_next_piece', + actions: { + '0': { + action: PlayoutActions.disableNextPiece, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'KeyG', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Disable the next element'), + }, + [IBlueprintDefaultCoreSystemTriggersType.disableNextPieceUndo]: { + _id: 'core_disable_next_piece_undo', + actions: { + '0': { + action: PlayoutActions.disableNextPiece, + filterChain: [ + { + object: 'view', + }, + ], + undo: true, + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Shift+KeyG', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Undo Disable the next element'), + }, + [IBlueprintDefaultCoreSystemTriggersType.createSnapshotForDebug]: { + _id: 'core_create_snapshot_for_debug', + actions: { + '0': { + action: PlayoutActions.createSnapshotForDebug, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Backspace', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Store Snapshot'), + }, + [IBlueprintDefaultCoreSystemTriggersType.moveNextPart]: { + _id: 'core_move_next_part', + actions: { + '0': { + action: PlayoutActions.moveNext, + filterChain: [ + { + object: 'view', + }, + ], + parts: 1, + segments: 0, + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'F9', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Move Next forwards'), + }, + [IBlueprintDefaultCoreSystemTriggersType.moveNextSegment]: { + _id: 'core_move_next_segment', + actions: { + '0': { + action: PlayoutActions.moveNext, + filterChain: [ + { + object: 'view', + }, + ], + parts: 0, + segments: 1, + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'F10', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Move Next to the following segment'), + }, + [IBlueprintDefaultCoreSystemTriggersType.movePreviousPart]: { + _id: 'core_move_previous_part', + actions: { + '0': { + action: PlayoutActions.moveNext, + filterChain: [ + { + object: 'view', + }, + ], + parts: -1, + segments: 0, + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Shift+F9', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Move Next backwards'), + }, + [IBlueprintDefaultCoreSystemTriggersType.movePreviousSegment]: { + _id: 'core_move_previous_segment', + actions: { + '0': { + action: PlayoutActions.moveNext, + filterChain: [ + { + object: 'view', + }, + ], + parts: 0, + segments: -1, + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Shift+F10', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Move Next to the previous segment'), + }, + [IBlueprintDefaultCoreSystemTriggersType.goToOnAirLine]: { + _id: 'core_go_to_onAir_line', + actions: { + '0': { + action: ClientActions.goToOnAirLine, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Control+Home', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Go to On Air line'), + }, + [IBlueprintDefaultCoreSystemTriggersType.rewindSegments]: { + _id: 'core_rewind_segments', + actions: { + '0': { + action: ClientActions.rewindSegments, + filterChain: [ + { + object: 'view', + }, + ], + }, + }, + triggers: { + '0': { + type: TriggerType.hotkey, + keys: 'Shift+Home', + up: true, + }, + }, + _rank: ++j * 1000, + name: t('Rewind segments to start'), + }, +} + +export const DEFAULT_CORE_TRIGGER_IDS = Object.values(DEFAULT_CORE_TRIGGERS).map( + (triggeredAction) => protectString(getHash(triggeredAction._id)) +) diff --git a/meteor/server/migration/upgrades/lib.ts b/meteor/server/migration/upgrades/lib.ts new file mode 100644 index 0000000000..ce825f2fd7 --- /dev/null +++ b/meteor/server/migration/upgrades/lib.ts @@ -0,0 +1,76 @@ +import type { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { TriggeredActions } from '../../collections' +import { Complete, getRandomId, literal, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' +import type { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' +import type { AnyBulkWriteOperation } from 'mongodb' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import type { IBlueprintTriggeredActions } from '@sofie-automation/blueprints-integration' + +export async function updateTriggeredActionsForShowStyleBaseId( + showStyleBaseId: ShowStyleBaseId | null, + triggeredActions: IBlueprintTriggeredActions[] +): Promise { + const oldTriggeredActionsArray = await TriggeredActions.findFetchAsync({ + showStyleBaseId: showStyleBaseId, + blueprintUniqueId: { $ne: null }, + }) + const oldTriggeredActions = normalizeArrayToMap(oldTriggeredActionsArray, 'blueprintUniqueId') + + const newDocIds: TriggeredActionId[] = [] + const bulkOps: AnyBulkWriteOperation[] = [] + + for (const newTriggeredAction of triggeredActions) { + const oldValue = oldTriggeredActions.get(newTriggeredAction._id) + if (oldValue) { + // Update an existing TriggeredAction + newDocIds.push(oldValue._id) + bulkOps.push({ + updateOne: { + filter: { + _id: oldValue._id, + }, + update: { + $set: { + _rank: newTriggeredAction._rank, + name: newTriggeredAction.name, + 'triggersWithOverrides.defaults': newTriggeredAction.triggers, + 'actionsWithOverrides.defaults': newTriggeredAction.actions, + }, + }, + }, + }) + } else { + // Insert a new TriggeredAction + const newDocId = getRandomId() + newDocIds.push(newDocId) + bulkOps.push({ + insertOne: { + document: literal>({ + _id: newDocId, + _rank: newTriggeredAction._rank, + name: newTriggeredAction.name, + showStyleBaseId: showStyleBaseId, + blueprintUniqueId: newTriggeredAction._id, + triggersWithOverrides: wrapDefaultObject(newTriggeredAction.triggers), + actionsWithOverrides: wrapDefaultObject(newTriggeredAction.actions), + styleClassNames: newTriggeredAction.styleClassNames, + }), + }, + }) + } + } + + // Remove any removed TriggeredAction + // Future: should this orphan them or something? Will that cause issues if they get re-added? + bulkOps.push({ + deleteMany: { + filter: { + showStyleBaseId: showStyleBaseId, + blueprintUniqueId: { $ne: null }, + _id: { $nin: newDocIds }, + }, + }, + }) + + await TriggeredActions.bulkWriteAsync(bulkOps) +} diff --git a/meteor/server/migration/upgrades/showStyleBase.ts b/meteor/server/migration/upgrades/showStyleBase.ts index d2281043ae..bcbe015617 100644 --- a/meteor/server/migration/upgrades/showStyleBase.ts +++ b/meteor/server/migration/upgrades/showStyleBase.ts @@ -3,25 +3,21 @@ import { JSONBlobParse, ShowStyleBlueprintManifest, } from '@sofie-automation/blueprints-integration' -import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { normalizeArray, normalizeArrayToMap, getRandomId, literal, Complete } from '@sofie-automation/corelib/dist/lib' -import { - applyAndValidateOverrides, - wrapDefaultObject, -} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { normalizeArray } from '@sofie-automation/corelib/dist/lib' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' import { Meteor } from 'meteor/meteor' -import { Blueprints, ShowStyleBases, TriggeredActions } from '../../collections' +import { Blueprints, ShowStyleBases } from '../../collections' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' import { evalBlueprint } from '../../api/blueprints/cache' import { logger } from '../../logging' import { CommonContext } from './context' -import type { AnyBulkWriteOperation } from 'mongodb' import { FixUpBlueprintConfigContext } from '@sofie-automation/corelib/dist/fixUpBlueprintConfig/context' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintFixUpConfigMessage } from '@sofie-automation/meteor-lib/dist/api/migration' +import { updateTriggeredActionsForShowStyleBaseId } from './lib' export async function fixupConfigForShowStyleBase( showStyleBaseId: ShowStyleBaseId @@ -100,7 +96,7 @@ export async function validateConfigForShowStyleBase( throwIfNeedsFixupConfigRunning(showStyleBase, blueprint, blueprintManifest) const blueprintContext = new CommonContext( - 'applyConfig', + 'validateConfig', `showStyleBase:${showStyleBaseId},blueprint:${blueprint._id}` ) const rawBlueprintConfig = applyAndValidateOverrides(showStyleBase.blueprintConfigWithOverrides).obj @@ -146,69 +142,7 @@ export async function runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseI }, }) - const oldTriggeredActionsArray = await TriggeredActions.findFetchAsync({ - showStyleBaseId: showStyleBaseId, - blueprintUniqueId: { $ne: null }, - }) - const oldTriggeredActions = normalizeArrayToMap(oldTriggeredActionsArray, 'blueprintUniqueId') - - const newDocIds: TriggeredActionId[] = [] - const bulkOps: AnyBulkWriteOperation[] = [] - - for (const newTriggeredAction of result.triggeredActions) { - const oldValue = oldTriggeredActions.get(newTriggeredAction._id) - if (oldValue) { - // Update an existing TriggeredAction - newDocIds.push(oldValue._id) - bulkOps.push({ - updateOne: { - filter: { - _id: oldValue._id, - }, - update: { - $set: { - _rank: newTriggeredAction._rank, - name: newTriggeredAction.name, - 'triggersWithOverrides.defaults': newTriggeredAction.triggers, - 'actionsWithOverrides.defaults': newTriggeredAction.actions, - }, - }, - }, - }) - } else { - // Insert a new TriggeredAction - const newDocId = getRandomId() - newDocIds.push(newDocId) - bulkOps.push({ - insertOne: { - document: literal>({ - _id: newDocId, - _rank: newTriggeredAction._rank, - name: newTriggeredAction.name, - showStyleBaseId: showStyleBaseId, - blueprintUniqueId: newTriggeredAction._id, - triggersWithOverrides: wrapDefaultObject(newTriggeredAction.triggers), - actionsWithOverrides: wrapDefaultObject(newTriggeredAction.actions), - styleClassNames: newTriggeredAction.styleClassNames, - }), - }, - }) - } - } - - // Remove any removed TriggeredAction - // Future: should this orphan them or something? Will that cause issues if they get re-added? - bulkOps.push({ - deleteMany: { - filter: { - showStyleBaseId: showStyleBaseId, - blueprintUniqueId: { $ne: null }, - _id: { $nin: newDocIds }, - }, - }, - }) - - await TriggeredActions.bulkWriteAsync(bulkOps) + await updateTriggeredActionsForShowStyleBaseId(showStyleBaseId, result.triggeredActions) } async function loadShowStyleAndBlueprint(showStyleBaseId: ShowStyleBaseId) { diff --git a/meteor/server/migration/upgrades/system.ts b/meteor/server/migration/upgrades/system.ts new file mode 100644 index 0000000000..15ae90bea8 --- /dev/null +++ b/meteor/server/migration/upgrades/system.ts @@ -0,0 +1,108 @@ +import { Meteor } from 'meteor/meteor' +import { logger } from '../../logging' +import { Blueprints, CoreSystem } from '../../collections' +import { + BlueprintManifestType, + BlueprintResultApplySystemConfig, + IBlueprintTriggeredActions, + SystemBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { evalBlueprint } from '../../api/blueprints/cache' +import { CoreSystemApplyConfigContext } from './context' +import { updateTriggeredActionsForShowStyleBaseId } from './lib' +import { CoreSystemId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DEFAULT_CORE_TRIGGERS } from './defaultSystemActionTriggers' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' + +export async function runUpgradeForCoreSystem(coreSystemId: CoreSystemId): Promise { + logger.info(`Running upgrade for CoreSystem`) + + const { coreSystem, blueprint, blueprintManifest } = await loadCoreSystemAndBlueprint(coreSystemId) + + let result: BlueprintResultApplySystemConfig + + if (blueprintManifest && typeof blueprintManifest.applyConfig === 'function') { + const blueprintContext = new CoreSystemApplyConfigContext( + 'applyConfig', + `coreSystem:${coreSystem._id},blueprint:${blueprint.blueprintId}` + ) + + result = blueprintManifest.applyConfig(blueprintContext) + } else { + // Ensure some defaults are populated when no blueprint method is present + result = generateDefaultSystemConfig() + } + + const coreSystemSettings: ICoreSystemSettings = result.settings + + await CoreSystem.updateAsync(coreSystemId, { + $set: { + 'settingsWithOverrides.defaults': coreSystemSettings, + lastBlueprintConfig: { + blueprintHash: blueprint?.blueprintHash ?? protectString('default'), + blueprintId: blueprint?._id ?? protectString('default'), + blueprintConfigPresetId: undefined, + config: {}, + }, + }, + }) + + await updateTriggeredActionsForShowStyleBaseId(null, result.triggeredActions) +} + +async function loadCoreSystemAndBlueprint(coreSystemId: CoreSystemId) { + const coreSystem = await CoreSystem.findOneAsync(coreSystemId) + if (!coreSystem) throw new Meteor.Error(404, `CoreSystem "${coreSystemId}" not found!`) + + if (!coreSystem.blueprintId) { + // No blueprint is valid + return { + coreSystem, + blueprint: undefined, + blueprintHash: undefined, + } + } + + // if (!showStyleBase.blueprintConfigPresetId) throw new Meteor.Error(500, 'ShowStyleBase is missing config preset') + + const blueprint = await Blueprints.findOneAsync({ + _id: coreSystem.blueprintId, + blueprintType: BlueprintManifestType.SYSTEM, + }) + if (!blueprint) throw new Meteor.Error(404, `Blueprint "${coreSystem.blueprintId}" not found!`) + + if (!blueprint.blueprintHash) throw new Meteor.Error(500, 'Blueprint is not valid') + + const blueprintManifest = evalBlueprint(blueprint) as SystemBlueprintManifest + + return { + coreSystem, + blueprint, + blueprintManifest, + } +} + +function generateDefaultSystemConfig(): BlueprintResultApplySystemConfig { + return { + settings: { + cron: { + casparCGRestart: { + enabled: true, + }, + storeRundownSnapshots: { + enabled: false, + }, + }, + support: { + message: '', + }, + evaluationsMessage: { + enabled: false, + heading: '', + message: '', + }, + }, + triggeredActions: Object.values(DEFAULT_CORE_TRIGGERS), + } +} diff --git a/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts index 5567ab0fb3..5fde9762ab 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts @@ -14,12 +14,13 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { generateTranslation } from '../../lib/tempLib' +import { generateTranslation, protectString } from '../../lib/tempLib' import { logger } from '../../logging' -import { ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import { CoreSystemFields, ShowStyleBaseFields, StudioFields } from './reactiveContentCache' import _ from 'underscore' import { UIBlueprintUpgradeStatusBase } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' export interface BlueprintMapEntry { _id: BlueprintId @@ -39,7 +40,7 @@ export function checkDocUpgradeStatus( // Studio blueprint is missing/invalid return { invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: doc.blueprintId, + blueprintId: doc.blueprintId ?? 'undefined', }), pendingRunOfFixupFunction: false, changes: [], @@ -101,7 +102,7 @@ export function checkDocUpgradeStatus( changes.push(generateTranslation('Blueprint has a new version')) } - if (doc.lastBlueprintConfig) { + if (doc.lastBlueprintConfig && doc.blueprintConfigWithOverrides) { // Check if the config blob has changed since last run const newConfig = applyAndValidateOverrides(doc.blueprintConfigWithOverrides).obj const oldConfig = doc.lastBlueprintConfig.config @@ -135,6 +136,65 @@ export function checkDocUpgradeStatus( } } +export function checkSystemUpgradeStatus( + blueprintMap: Map, + doc: Pick +): Pick { + const changes: ITranslatableMessage[] = [] + + // Check the blueprintId is valid + if (doc.blueprintId) { + const blueprint = blueprintMap.get(doc.blueprintId) + if (!blueprint || !blueprint.configPresets) { + // Studio blueprint is missing/invalid + return { + invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { + blueprintId: doc.blueprintId ?? 'undefined', + }), + pendingRunOfFixupFunction: false, + changes: [], + } + } + + // Some basic property checks + if (!doc.lastBlueprintConfig) { + changes.push(generateTranslation('Config has not been applied before')) + } else if (doc.lastBlueprintConfig.blueprintId !== doc.blueprintId) { + changes.push( + generateTranslation('Blueprint has been changed. From "{{ oldValue }}", to "{{ newValue }}"', { + oldValue: doc.lastBlueprintConfig.blueprintId || '', + newValue: doc.blueprintId || '', + }) + ) + } else if (doc.lastBlueprintConfig.blueprintHash !== blueprint.blueprintHash) { + changes.push(generateTranslation('Blueprint has a new version')) + } + } else { + // No blueprint assigned + + const defaultId = protectString('default') + + // Some basic property checks + if (!doc.lastBlueprintConfig) { + changes.push(generateTranslation('Config has not been applied before')) + } else if (doc.lastBlueprintConfig.blueprintId !== defaultId) { + changes.push( + generateTranslation('Blueprint has been changed. From "{{ oldValue }}", to "{{ newValue }}"', { + oldValue: doc.lastBlueprintConfig.blueprintId || '', + newValue: defaultId, + }) + ) + } else if (doc.lastBlueprintConfig.blueprintHash !== defaultId) { + changes.push(generateTranslation('Blueprint has a new version')) + } + } + + return { + changes, + pendingRunOfFixupFunction: false, + } +} + /** * This is a slightly crude diffing of objects based on a jsonschema. Only keys in the schema will be compared. * For now this has some limitations such as not looking inside of arrays, but this could be expanded later on diff --git a/meteor/server/publications/blueprintUpgradeStatus/publication.ts b/meteor/server/publications/blueprintUpgradeStatus/publication.ts index 1bebd28c49..00df57b838 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/publication.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/publication.ts @@ -10,9 +10,15 @@ import { SetupObserversResult, TriggerUpdate, } from '../../lib/customPublication' -import { ContentCache, createReactiveContentCache, ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import { + ContentCache, + CoreSystemFields, + createReactiveContentCache, + ShowStyleBaseFields, + StudioFields, +} from './reactiveContentCache' import { UpgradesContentObserver } from './upgradesContentObserver' -import { BlueprintMapEntry, checkDocUpgradeStatus } from './checkStatus' +import { BlueprintMapEntry, checkDocUpgradeStatus, checkSystemUpgradeStatus } from './checkStatus' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' @@ -20,6 +26,7 @@ import { UIBlueprintUpgradeStatus, UIBlueprintUpgradeStatusId, } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { assertConnectionHasOneOfPermissions } from '../../security/auth' type BlueprintUpgradeStatusArgs = Record @@ -31,6 +38,7 @@ export interface BlueprintUpgradeStatusState { interface BlueprintUpgradeStatusUpdateProps { newCache: ContentCache + invalidateSystem: boolean invalidateStudioIds: StudioId[] invalidateShowStyleBaseIds: ShowStyleBaseId[] invalidateBlueprintIds: BlueprintId[] @@ -52,6 +60,11 @@ async function setupBlueprintUpgradeStatusPublicationObservers( return [ mongoObserver, + cache.CoreSystem.find({}).observeChanges({ + added: () => triggerUpdate({ invalidateSystem: true }), + changed: () => triggerUpdate({ invalidateSystem: true }), + removed: () => triggerUpdate({ invalidateSystem: true }), + }), cache.Studios.find({}).observeChanges({ added: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), changed: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), @@ -70,7 +83,10 @@ async function setupBlueprintUpgradeStatusPublicationObservers( ] } -function getDocumentId(type: 'studio' | 'showStyle', id: ProtectedString): UIBlueprintUpgradeStatusId { +function getDocumentId( + type: 'coreSystem' | 'studio' | 'showStyle', + id: ProtectedString +): UIBlueprintUpgradeStatusId { return protectString(`${type}:${id}`) } @@ -98,6 +114,7 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( const studioBlueprintsMap = new Map() const showStyleBlueprintsMap = new Map() + const systemBlueprintsMap = new Map() state.contentCache.Blueprints.find({}).forEach((blueprint) => { switch (blueprint.blueprintType) { case BlueprintManifestType.SHOWSTYLE: @@ -118,6 +135,15 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( hasFixUpFunction: blueprint.hasFixUpFunction, }) break + case BlueprintManifestType.SYSTEM: + systemBlueprintsMap.set(blueprint._id, { + _id: blueprint._id, + configPresets: {}, + configSchema: undefined, // TODO + blueprintHash: blueprint.blueprintHash, + hasFixUpFunction: false, + }) + break // TODO - default? } }) @@ -134,6 +160,10 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( state.contentCache.ShowStyleBases.find({}).forEach((showStyleBase) => { updateShowStyleUpgradeStatus(collection, showStyleBlueprintsMap, showStyleBase) }) + + state.contentCache.CoreSystem.find({}).forEach((coreSystem) => { + updateCoreSystemUpgradeStatus(collection, systemBlueprintsMap, coreSystem) + }) } else { const regenerateForStudioIds = new Set(updateProps.invalidateStudioIds) const regenerateForShowStyleBaseIds = new Set(updateProps.invalidateShowStyleBaseIds) @@ -179,9 +209,31 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( collection.remove(getDocumentId('showStyle', showStyleBaseId)) } } + + if (updateProps.invalidateSystem) { + state.contentCache.CoreSystem.find({}).forEach((coreSystem) => { + updateCoreSystemUpgradeStatus(collection, systemBlueprintsMap, coreSystem) + }) + } } } +function updateCoreSystemUpgradeStatus( + collection: CustomPublishCollection, + blueprintsMap: Map, + coreSystem: Pick +) { + const status = checkSystemUpgradeStatus(blueprintsMap, coreSystem) + + collection.replace({ + ...status, + _id: getDocumentId('coreSystem', coreSystem._id), + documentType: 'coreSystem', + documentId: coreSystem._id, + name: coreSystem.name ?? 'System', + }) +} + function updateStudioUpgradeStatus( collection: CustomPublishCollection, blueprintsMap: Map, diff --git a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts index 501e678062..1aa3474720 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts @@ -4,6 +4,25 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' + +export type CoreSystemFields = + | '_id' + | 'blueprintId' + | 'blueprintConfigPresetId' + | 'lastBlueprintConfig' + | 'blueprintConfigWithOverrides' + | 'lastBlueprintFixUpHash' + | 'name' +export const coreSystemFieldsSpecifier = literal>>({ + _id: 1, + blueprintId: 1, + blueprintConfigPresetId: 1, + lastBlueprintConfig: 1, + lastBlueprintFixUpHash: 1, + blueprintConfigWithOverrides: 1, + name: 1, +}) export type StudioFields = | '_id' @@ -64,6 +83,7 @@ export const blueprintFieldSpecifier = literal> Studios: ReactiveCacheCollection> ShowStyleBases: ReactiveCacheCollection> Blueprints: ReactiveCacheCollection> @@ -71,6 +91,7 @@ export interface ContentCache { export function createReactiveContentCache(): ContentCache { const cache: ContentCache = { + CoreSystem: new ReactiveCacheCollection>('coreSystem'), Studios: new ReactiveCacheCollection>('studios'), ShowStyleBases: new ReactiveCacheCollection>('showStyleBases'), Blueprints: new ReactiveCacheCollection>('blueprints'), diff --git a/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts index a88ba8575b..e8f8d6281a 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts @@ -3,10 +3,11 @@ import { logger } from '../../logging' import { blueprintFieldSpecifier, ContentCache, + coreSystemFieldsSpecifier, showStyleFieldSpecifier, studioFieldSpecifier, } from './reactiveContentCache' -import { Blueprints, ShowStyleBases, Studios } from '../../collections' +import { Blueprints, CoreSystem, ShowStyleBases, Studios } from '../../collections' import { waitForAllObserversReady } from '../lib/lib' export class UpgradesContentObserver { @@ -22,6 +23,9 @@ export class UpgradesContentObserver { logger.silly(`Creating UpgradesContentObserver`) const observers = await waitForAllObserversReady([ + CoreSystem.observeChanges({}, cache.CoreSystem.link(), { + projection: coreSystemFieldsSpecifier, + }), Studios.observeChanges({}, cache.Studios.link(), { projection: studioFieldSpecifier, }), diff --git a/meteor/server/publications/lib/quickLoop.ts b/meteor/server/publications/lib/quickLoop.ts index 272a554ac9..9b4bb08374 100644 --- a/meteor/server/publications/lib/quickLoop.ts +++ b/meteor/server/publications/lib/quickLoop.ts @@ -1,16 +1,16 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBRundownPlaylist, - ForceQuickLoopAutoNext, QuickLoopMarker, QuickLoopMarkerType, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/corelib/dist/playout/playlist' import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { DEFAULT_FALLBACK_PART_DURATION } from '@sofie-automation/shared-lib/dist/core/constants' import { getCurrentTime } from '../../lib/lib' import { generateTranslation } from '@sofie-automation/corelib/dist/lib' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' @@ -47,7 +47,7 @@ export function modifyPartForQuickLoop( segmentRanks: Record, rundownRanks: Record, playlist: Pick, - studio: Pick, + studioSettings: IStudioSettings, quickLoopStartPosition: MarkerPosition | undefined, quickLoopEndPosition: MarkerPosition | undefined, canSetAutoNext = () => true @@ -60,7 +60,7 @@ export function modifyPartForQuickLoop( compareMarkerPositions(quickLoopStartPosition, partPosition) >= 0 && compareMarkerPositions(partPosition, quickLoopEndPosition) >= 0 - const fallbackPartDuration = studio.settings.fallbackPartDuration ?? DEFAULT_FALLBACK_PART_DURATION + const fallbackPartDuration = studioSettings.fallbackPartDuration ?? DEFAULT_FALLBACK_PART_DURATION if (isLoopingOverriden && (part.expectedDuration ?? 0) < fallbackPartDuration) { if (playlist.quickLoop?.forceAutoNext === ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION) { @@ -82,7 +82,7 @@ export function modifyPartInstanceForQuickLoop( segmentRanks: Record, rundownRanks: Record, playlist: Pick, - studio: Pick, + studioSettings: IStudioSettings, quickLoopStartPosition: MarkerPosition | undefined, quickLoopEndPosition: MarkerPosition | undefined ): void { @@ -107,7 +107,7 @@ export function modifyPartInstanceForQuickLoop( segmentRanks, rundownRanks, playlist, - studio, + studioSettings, quickLoopStartPosition, quickLoopEndPosition, canAutoNext // do not adjust the part instance if we have passed the time where we can still enable auto next diff --git a/meteor/server/publications/partInstancesUI/publication.ts b/meteor/server/publications/partInstancesUI/publication.ts index 8a16d6af5f..ede1616f19 100644 --- a/meteor/server/publications/partInstancesUI/publication.ts +++ b/meteor/server/publications/partInstancesUI/publication.ts @@ -105,7 +105,7 @@ async function setupUIPartInstancesPublicationObservers( changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), }), - cache.Studios.find({}).observeChanges({ + cache.StudioSettings.find({}).observeChanges({ added: () => triggerUpdate({ invalidateQuickLoop: true }), changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), @@ -147,8 +147,8 @@ export async function manipulateUIPartInstancesPublicationData( const playlist = state.contentCache.RundownPlaylists.findOne({}) if (!playlist) return - const studio = state.contentCache.Studios.findOne({}) - if (!studio) return + const studioSettings = state.contentCache.StudioSettings.findOne({}) + if (!studioSettings) return const rundownRanks = stringsToIndexLookup(playlist.rundownIdsInOrder as unknown as string[]) const segmentRanks = extractRanks(state.contentCache.Segments.find({}).fetch()) @@ -190,7 +190,7 @@ export async function manipulateUIPartInstancesPublicationData( segmentRanks, rundownRanks, playlist, - studio, + studioSettings.settings, quickLoopStartPosition, quickLoopEndPosition ) diff --git a/meteor/server/publications/partInstancesUI/reactiveContentCache.ts b/meteor/server/publications/partInstancesUI/reactiveContentCache.ts index b9356fb6a1..a647ffd79c 100644 --- a/meteor/server/publications/partInstancesUI/reactiveContentCache.ts +++ b/meteor/server/publications/partInstancesUI/reactiveContentCache.ts @@ -3,8 +3,9 @@ import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict, MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' export type RundownPlaylistCompact = Pick export const rundownPlaylistFieldSpecifier = literal>({ @@ -27,14 +28,19 @@ export const partInstanceFieldSpecifier = literal>>({ _id: 1, - settings: 1, + settingsWithOverrides: 1, }) +export interface StudioSettingsDoc { + _id: StudioId + settings: IStudioSettings +} + export interface ContentCache { - Studios: ReactiveCacheCollection> + StudioSettings: ReactiveCacheCollection Segments: ReactiveCacheCollection> PartInstances: ReactiveCacheCollection> RundownPlaylists: ReactiveCacheCollection @@ -42,7 +48,7 @@ export interface ContentCache { export function createReactiveContentCache(): ContentCache { const cache: ContentCache = { - Studios: new ReactiveCacheCollection>('studios'), + StudioSettings: new ReactiveCacheCollection('studioSettings'), Segments: new ReactiveCacheCollection>('segments'), PartInstances: new ReactiveCacheCollection>('partInstances'), RundownPlaylists: new ReactiveCacheCollection('rundownPlaylists'), diff --git a/meteor/server/publications/partInstancesUI/rundownContentObserver.ts b/meteor/server/publications/partInstancesUI/rundownContentObserver.ts index 09c782f709..bc6e06ebfd 100644 --- a/meteor/server/publications/partInstancesUI/rundownContentObserver.ts +++ b/meteor/server/publications/partInstancesUI/rundownContentObserver.ts @@ -6,10 +6,21 @@ import { partInstanceFieldSpecifier, rundownPlaylistFieldSpecifier, segmentFieldSpecifier, + StudioFields, studioFieldSpecifier, + StudioSettingsDoc, } from './reactiveContentCache' import { PartInstances, RundownPlaylists, Segments, Studios } from '../../collections' import { waitForAllObserversReady } from '../lib/lib' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' + +function convertStudioSettingsDoc(doc: Pick): StudioSettingsDoc { + return { + _id: doc._id, + settings: applyAndValidateOverrides(doc.settingsWithOverrides).obj, + } +} export class RundownContentObserver { readonly #cache: ContentCache @@ -30,11 +41,23 @@ export class RundownContentObserver { logger.silly(`Creating RundownContentObserver for rundowns "${rundownIds.join(',')}"`) const observers = await waitForAllObserversReady([ - Studios.observeChanges( + Studios.observe( { _id: studioId, }, - cache.Studios.link(), + { + added: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + changed: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + removed: (doc) => { + cache.StudioSettings.remove(doc._id) + }, + }, { fields: studioFieldSpecifier, } diff --git a/meteor/server/publications/partsUI/publication.ts b/meteor/server/publications/partsUI/publication.ts index 69bfc890be..24460ab13c 100644 --- a/meteor/server/publications/partsUI/publication.ts +++ b/meteor/server/publications/partsUI/publication.ts @@ -91,7 +91,7 @@ async function setupUIPartsPublicationObservers( changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), }), - cache.Studios.find({}).observeChanges({ + cache.StudioSettings.find({}).observeChanges({ added: () => triggerUpdate({ invalidateQuickLoop: true }), changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), @@ -133,8 +133,8 @@ export async function manipulateUIPartsPublicationData( const playlist = state.contentCache.RundownPlaylists.findOne({}) if (!playlist) return - const studio = state.contentCache.Studios.findOne({}) - if (!studio) return + const studioSettings = state.contentCache.StudioSettings.findOne({}) + if (!studioSettings) return const rundownRanks = stringsToIndexLookup(playlist.rundownIdsInOrder as unknown as string[]) const segmentRanks = extractRanks(state.contentCache.Segments.find({}).fetch()) @@ -176,7 +176,7 @@ export async function manipulateUIPartsPublicationData( segmentRanks, rundownRanks, playlist, - studio, + studioSettings.settings, quickLoopStartPosition, quickLoopEndPosition ) diff --git a/meteor/server/publications/partsUI/reactiveContentCache.ts b/meteor/server/publications/partsUI/reactiveContentCache.ts index 13361fd51c..12d9423e8e 100644 --- a/meteor/server/publications/partsUI/reactiveContentCache.ts +++ b/meteor/server/publications/partsUI/reactiveContentCache.ts @@ -4,7 +4,8 @@ import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict, MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' export type RundownPlaylistCompact = Pick export const rundownPlaylistFieldSpecifier = literal>({ @@ -26,14 +27,19 @@ export const partFieldSpecifier = literal>>({ _id: 1, - settings: 1, + settingsWithOverrides: 1, }) +export interface StudioSettingsDoc { + _id: StudioId + settings: IStudioSettings +} + export interface ContentCache { - Studios: ReactiveCacheCollection> + StudioSettings: ReactiveCacheCollection Segments: ReactiveCacheCollection> Parts: ReactiveCacheCollection> RundownPlaylists: ReactiveCacheCollection @@ -41,7 +47,7 @@ export interface ContentCache { export function createReactiveContentCache(): ContentCache { const cache: ContentCache = { - Studios: new ReactiveCacheCollection>('studios'), + StudioSettings: new ReactiveCacheCollection('studioSettings'), Segments: new ReactiveCacheCollection>('segments'), Parts: new ReactiveCacheCollection>('parts'), RundownPlaylists: new ReactiveCacheCollection('rundownPlaylists'), diff --git a/meteor/server/publications/partsUI/rundownContentObserver.ts b/meteor/server/publications/partsUI/rundownContentObserver.ts index ee7e92c7d6..8a8032ecf5 100644 --- a/meteor/server/publications/partsUI/rundownContentObserver.ts +++ b/meteor/server/publications/partsUI/rundownContentObserver.ts @@ -6,10 +6,21 @@ import { partFieldSpecifier, rundownPlaylistFieldSpecifier, segmentFieldSpecifier, + StudioFields, studioFieldSpecifier, + StudioSettingsDoc, } from './reactiveContentCache' import { Parts, RundownPlaylists, Segments, Studios } from '../../collections' import { waitForAllObserversReady } from '../lib/lib' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' + +function convertStudioSettingsDoc(doc: Pick): StudioSettingsDoc { + return { + _id: doc._id, + settings: applyAndValidateOverrides(doc.settingsWithOverrides).obj, + } +} export class RundownContentObserver { readonly #cache: ContentCache @@ -29,11 +40,23 @@ export class RundownContentObserver { logger.silly(`Creating RundownContentObserver for rundowns "${rundownIds.join(',')}"`) const observers = await waitForAllObserversReady([ - Studios.observeChanges( + Studios.observe( { _id: studioId, }, - cache.Studios.link(), + { + added: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + changed: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + removed: (doc) => { + cache.StudioSettings.remove(doc._id) + }, + }, { fields: studioFieldSpecifier, } diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index dbeb8a4e9e..dbead8658e 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -187,7 +187,7 @@ export type PieceContentStatusPiece = Pick { + extends Pick { /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt /** Route sets with overrides */ @@ -196,6 +196,8 @@ export interface PieceContentStatusStudio * (These are used by the Package Manager and the Expected Packages) */ packageContainers: Record + + settings: IStudioSettings } export async function checkPieceContentStatusAndDependencies( diff --git a/meteor/server/publications/pieceContentStatusUI/common.ts b/meteor/server/publications/pieceContentStatusUI/common.ts index 591f1eb16e..32cf9328d1 100644 --- a/meteor/server/publications/pieceContentStatusUI/common.ts +++ b/meteor/server/publications/pieceContentStatusUI/common.ts @@ -13,7 +13,7 @@ import { PieceContentStatusStudio } from './checkPieceContentStatus' export type StudioFields = | '_id' - | 'settings' + | 'settingsWithOverrides' | 'packageContainersWithOverrides' | 'previewContainerIds' | 'thumbnailContainerIds' @@ -21,7 +21,7 @@ export type StudioFields = | 'routeSetsWithOverrides' export const studioFieldSpecifier = literal>>({ _id: 1, - settings: 1, + settingsWithOverrides: 1, packageContainersWithOverrides: 1, previewContainerIds: 1, thumbnailContainerIds: 1, @@ -112,7 +112,7 @@ export async function fetchStudio(studioId: StudioId): Promise): UIStudio { name: studio.name, mappings: applyAndValidateOverrides(studio.mappingsWithOverrides).obj, - settings: studio.settings, + settings: applyAndValidateOverrides(studio.settingsWithOverrides).obj, routeSets: applyAndValidateOverrides(studio.routeSetsWithOverrides).obj, routeSetExclusivityGroups: applyAndValidateOverrides(studio.routeSetExclusivityGroupsWithOverrides).obj, @@ -44,14 +44,14 @@ type StudioFields = | '_id' | 'name' | 'mappingsWithOverrides' - | 'settings' + | 'settingsWithOverrides' | 'routeSetsWithOverrides' | 'routeSetExclusivityGroupsWithOverrides' const fieldSpecifier = literal>>({ _id: 1, name: 1, mappingsWithOverrides: 1, - settings: 1, + settingsWithOverrides: 1, routeSetsWithOverrides: 1, routeSetExclusivityGroupsWithOverrides: 1, }) diff --git a/meteor/server/publications/system.ts b/meteor/server/publications/system.ts index b1629b6425..dd2b8ee2b2 100644 --- a/meteor/server/publications/system.ts +++ b/meteor/server/publications/system.ts @@ -11,16 +11,14 @@ meteorPublish(MeteorPubSub.coreSystem, async function (_token: string | undefine fields: { // Include only specific fields in the result documents: _id: 1, - support: 1, systemInfo: 1, apm: 1, name: 1, logLevel: 1, serviceMessages: 1, blueprintId: 1, - cron: 1, logo: 1, - evaluations: 1, + settingsWithOverrides: 1, }, }) }) diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 9a4958ea28..c318ab1cf1 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -21,7 +21,7 @@ import type { } from '../context' import type { IngestAdlib, ExtendedIngestRundown, IngestRundown } from '../ingest' import type { IBlueprintExternalMessageQueueObj } from '../message' -import type { MigrationStepShowStyle } from '../migrations' +import type {} from '../migrations' import type { IBlueprintAdLibPiece, IBlueprintResolvedPieceInstance, @@ -56,10 +56,6 @@ export interface ShowStyleBlueprintManifest - /** A list of Migration steps related to a ShowStyle - * @deprecated This has been replaced with `validateConfig` and `applyConfig` - */ - showStyleMigrations: MigrationStepShowStyle[] /** The config presets exposed by this blueprint */ configPresets: Record> diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 098bbc2283..32031f9d65 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -3,7 +3,6 @@ import type { ReadonlyDeep } from 'type-fest' import type { BlueprintConfigCoreConfig, BlueprintManifestBase, BlueprintManifestType, IConfigMessage } from './base' import type { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import type { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' -import type { MigrationStepStudio } from '../migrations' import type { ICommonContext, IFixUpConfigContext, @@ -27,7 +26,8 @@ import type { StudioRouteSet, StudioRouteSetExclusivityGroup, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' -import { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import type { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import type { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' export interface StudioBlueprintManifest extends BlueprintManifestBase { @@ -35,10 +35,6 @@ export interface StudioBlueprintManifest - /** A list of Migration steps related to a Studio - * @deprecated This has been replaced with `validateConfig` and `applyConfig` - */ - studioMigrations: MigrationStepStudio[] /** The config presets exposed by this blueprint */ configPresets: Record> @@ -162,6 +158,8 @@ export interface BlueprintResultApplyStudioConfig { routeSetExclusivityGroups?: Record /** Package Containers */ packageContainers?: Record + + studioSettings?: IStudioSettings } export interface BlueprintParentDeviceSettings { /** diff --git a/packages/blueprints-integration/src/api/system.ts b/packages/blueprints-integration/src/api/system.ts index 078ab6ffed..a4c544d571 100644 --- a/packages/blueprints-integration/src/api/system.ts +++ b/packages/blueprints-integration/src/api/system.ts @@ -1,12 +1,32 @@ +import type { IBlueprintTriggeredActions } from '../triggers' import type { MigrationStepSystem } from '../migrations' import type { BlueprintManifestBase, BlueprintManifestType } from './base' +import type { ICoreSystemApplyConfigContext } from '../context/systemApplyConfigContext' +import type { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' export interface SystemBlueprintManifest extends BlueprintManifestBase { blueprintType: BlueprintManifestType.SYSTEM - /** A list of Migration steps related to the Core system */ + /** A list of Migration steps related to the Core system + * @deprecated This has been replaced with `applyConfig` + */ coreMigrations: MigrationStepSystem[] /** Translations connected to the studio (as stringified JSON) */ translations?: string + + /** + * Apply the config by generating the data to be saved into the db. + * This should be written to give a predictable and stable result, it can be called with the same config multiple times + */ + applyConfig?: ( + context: ICoreSystemApplyConfigContext + // config: TRawConfig, + ) => BlueprintResultApplySystemConfig +} + +export interface BlueprintResultApplySystemConfig { + settings: ICoreSystemSettings + + triggeredActions: IBlueprintTriggeredActions[] } diff --git a/packages/blueprints-integration/src/context/systemApplyConfigContext.ts b/packages/blueprints-integration/src/context/systemApplyConfigContext.ts new file mode 100644 index 0000000000..c1878ed75c --- /dev/null +++ b/packages/blueprints-integration/src/context/systemApplyConfigContext.ts @@ -0,0 +1,6 @@ +import type { IBlueprintDefaultCoreSystemTriggers } from '../triggers' +import type { ICommonContext } from './baseContext' + +export interface ICoreSystemApplyConfigContext extends ICommonContext { + getDefaultSystemActionTriggers(): IBlueprintDefaultCoreSystemTriggers +} diff --git a/packages/blueprints-integration/src/index.ts b/packages/blueprints-integration/src/index.ts index d5196e59f7..4eb1fa41a5 100644 --- a/packages/blueprints-integration/src/index.ts +++ b/packages/blueprints-integration/src/index.ts @@ -28,3 +28,5 @@ export { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaType export * from '@sofie-automation/shared-lib/dist/lib/JSONBlob' export * from '@sofie-automation/shared-lib/dist/lib/JSONSchemaUtil' export * from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' +export * from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +export * from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' diff --git a/packages/blueprints-integration/src/migrations.ts b/packages/blueprints-integration/src/migrations.ts index 6309e8cc90..62d3d4913f 100644 --- a/packages/blueprints-integration/src/migrations.ts +++ b/packages/blueprints-integration/src/migrations.ts @@ -1,9 +1,4 @@ -import { ConfigItemValue } from './common' -import { OmitId } from './lib' -import { IBlueprintShowStyleVariant, IOutputLayer, ISourceLayer } from './showStyle' import { IBlueprintTriggeredActions } from './triggers' -import { BlueprintMapping } from './studio' -import { TSR } from './timeline' export interface MigrationStepInput { stepId?: string // automatically filled in later @@ -28,59 +23,18 @@ export type ValidateFunctionSystem = ( context: MigrationContextSystem, afterMigration: boolean ) => Promise -export type ValidateFunctionStudio = (context: MigrationContextStudio, afterMigration: boolean) => boolean | string -export type ValidateFunctionShowStyle = ( - context: MigrationContextShowStyle, - afterMigration: boolean -) => boolean | string -export type ValidateFunction = - | ValidateFunctionStudio - | ValidateFunctionShowStyle - | ValidateFunctionSystem - | ValidateFunctionCore +export type ValidateFunction = ValidateFunctionSystem | ValidateFunctionCore export type MigrateFunctionCore = (input: MigrationStepInputFilteredResult) => Promise export type MigrateFunctionSystem = ( context: MigrationContextSystem, input: MigrationStepInputFilteredResult ) => Promise -export type MigrateFunctionStudio = (context: MigrationContextStudio, input: MigrationStepInputFilteredResult) => void -export type MigrateFunctionShowStyle = ( - context: MigrationContextShowStyle, - input: MigrationStepInputFilteredResult -) => void -export type MigrateFunction = - | MigrateFunctionStudio - | MigrateFunctionShowStyle - | MigrateFunctionSystem - | MigrateFunctionCore +export type MigrateFunction = MigrateFunctionSystem | MigrateFunctionCore export type InputFunctionCore = () => MigrationStepInput[] export type InputFunctionSystem = (context: MigrationContextSystem) => MigrationStepInput[] -export type InputFunctionStudio = (context: MigrationContextStudio) => MigrationStepInput[] -export type InputFunctionShowStyle = (context: MigrationContextShowStyle) => MigrationStepInput[] -export type InputFunction = InputFunctionStudio | InputFunctionShowStyle | InputFunctionSystem | InputFunctionCore - -export interface MigrationContextStudio { - getMapping: (mappingId: string) => BlueprintMapping | undefined - insertMapping: (mappingId: string, mapping: OmitId) => string - updateMapping: (mappingId: string, mapping: Partial) => void - removeMapping: (mappingId: string) => void - - getConfig: (configId: string) => ConfigItemValue | undefined - setConfig: (configId: string, value: ConfigItemValue) => void - removeConfig: (configId: string) => void - - getDevice: (deviceId: string) => TSR.DeviceOptionsAny | undefined - insertDevice: (deviceId: string, device: TSR.DeviceOptionsAny) => string | null - updateDevice: (deviceId: string, device: Partial) => void - removeDevice: (deviceId: string) => void -} - -export interface ShowStyleVariantPart { - // Note: if more props are added it may make sense to use Omit<> to build this type - name: string -} +export type InputFunction = InputFunctionSystem | InputFunctionCore interface MigrationContextWithTriggeredActions { getAllTriggeredActions: () => Promise @@ -90,33 +44,6 @@ interface MigrationContextWithTriggeredActions { removeTriggeredAction: (triggeredActionId: string) => Promise } -export interface MigrationContextShowStyle extends MigrationContextWithTriggeredActions { - getAllVariants: () => IBlueprintShowStyleVariant[] - getVariantId: (variantId: string) => string - getVariant: (variantId: string) => IBlueprintShowStyleVariant | undefined - insertVariant: (variantId: string, variant: OmitId) => string - updateVariant: (variantId: string, variant: Partial) => void - removeVariant: (variantId: string) => void - - getSourceLayer: (sourceLayerId: string) => ISourceLayer | undefined - insertSourceLayer: (sourceLayerId: string, layer: OmitId) => string - updateSourceLayer: (sourceLayerId: string, layer: Partial) => void - removeSourceLayer: (sourceLayerId: string) => void - - getOutputLayer: (outputLayerId: string) => IOutputLayer | undefined - insertOutputLayer: (outputLayerId: string, layer: OmitId) => string - updateOutputLayer: (outputLayerId: string, layer: Partial) => void - removeOutputLayer: (outputLayerId: string) => void - - getBaseConfig: (configId: string) => ConfigItemValue | undefined - setBaseConfig: (configId: string, value: ConfigItemValue) => void - removeBaseConfig: (configId: string) => void - - getVariantConfig: (variantId: string, configId: string) => ConfigItemValue | undefined - setVariantConfig: (variantId: string, configId: string, value: ConfigItemValue) => void - removeVariantConfig: (variantId: string, configId: string) => void -} - export type MigrationContextSystem = MigrationContextWithTriggeredActions export interface MigrationStepBase< @@ -163,9 +90,3 @@ export interface MigrationStep< export type MigrationStepCore = MigrationStep export type MigrationStepSystem = MigrationStep -export type MigrationStepStudio = MigrationStep -export type MigrationStepShowStyle = MigrationStep< - ValidateFunctionShowStyle, - MigrateFunctionShowStyle, - InputFunctionShowStyle -> diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index 3b7a54db85..c360fa6567 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -340,3 +340,27 @@ export interface IBlueprintTriggeredActions { } export { SomeActionIdentifier, ClientActions, PlayoutActions } + +export enum IBlueprintDefaultCoreSystemTriggersType { + toggleShelf = 'toggleShelf', + activateRundownPlaylist = 'activateRundownPlaylist', + activateRundownPlaylistRehearsal = 'activateRundownPlaylistRehearsal', + deactivateRundownPlaylist = 'deactivateRundownPlaylist', + take = 'take', + hold = 'hold', + holdUndo = 'holdUndo', + resetRundownPlaylist = 'resetRundownPlaylist', + disableNextPiece = 'disableNextPiece', + disableNextPieceUndo = 'disableNextPieceUndo', + createSnapshotForDebug = 'createSnapshotForDebug', + moveNextPart = 'moveNextPart', + moveNextSegment = 'moveNextSegment', + movePreviousPart = 'movePreviousPart', + movePreviousSegment = 'movePreviousSegment', + goToOnAirLine = 'goToOnAirLine', + rewindSegments = 'rewindSegments', +} + +export type IBlueprintDefaultCoreSystemTriggers = { + [key in IBlueprintDefaultCoreSystemTriggersType]: IBlueprintTriggeredActions +} diff --git a/packages/corelib/src/dataModel/Blueprint.ts b/packages/corelib/src/dataModel/Blueprint.ts index 99e025bfdf..2ca55d4e2e 100644 --- a/packages/corelib/src/dataModel/Blueprint.ts +++ b/packages/corelib/src/dataModel/Blueprint.ts @@ -38,12 +38,6 @@ export interface Blueprint { showStyleConfigPresets?: Record databaseVersion: { - showStyle: { - [showStyleBaseId: string]: string - } - studio: { - [studioId: string]: string - } system: string | undefined } @@ -64,7 +58,7 @@ export interface Blueprint { export interface LastBlueprintConfig { blueprintId: BlueprintId blueprintHash: BlueprintHash - blueprintConfigPresetId: string + blueprintConfigPresetId: string | undefined config: IBlueprintConfig } diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index 241e0c3895..f9ff938c02 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -11,6 +11,7 @@ import { RundownId, } from './Ids' import { RundownPlaylistNote } from './Notes' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /** Details of an ab-session requested by the blueprints in onTimelineGenerate */ export interface ABSessionInfo { @@ -80,15 +81,6 @@ export type QuickLoopMarker = | QuickLoopRundownMarker | QuickLoopPlaylistMarker -export enum ForceQuickLoopAutoNext { - /** Parts will auto-next only when explicitly set by the NRCS/blueprints */ - DISABLED = 'disabled', - /** Parts will auto-next when the expected duration is set and within range */ - ENABLED_WHEN_VALID_DURATION = 'enabled_when_valid_duration', - /** All parts will auto-next. If expected duration is undefined or low, the default display duration will be used */ - ENABLED_FORCING_MIN_DURATION = 'enabled_forcing_min_duration', -} - export interface QuickLoopProps { /** The Start marker */ start?: QuickLoopMarker diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index f4a70a9073..dad72ea2ea 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -3,7 +3,6 @@ import { ObjectWithOverrides } from '../settings/objectWithOverrides' import { StudioId, OrganizationId, BlueprintId, ShowStyleBaseId, MappingsHash, PeripheralDeviceId } from './Ids' import { BlueprintHash, LastBlueprintConfig } from './Blueprint' import { MappingsExt, MappingExt } from '@sofie-automation/shared-lib/dist/core/model/Timeline' -import { ForceQuickLoopAutoNext } from './RundownPlaylist' import { ResultingMappingRoute, RouteMapping, @@ -15,8 +14,9 @@ import { StudioAbPlayerDisabling, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' import { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' -export { MappingsExt, MappingExt, MappingsHash } +export { MappingsExt, MappingExt, MappingsHash, IStudioSettings } // RouteSet functions has been moved to shared-lib: // So we need to re-export them here: @@ -32,82 +32,6 @@ export { StudioPackageContainer, } -export interface IStudioSettings { - /** The framerate (frames per second) used to convert internal timing information (in milliseconds) - * into timecodes and timecode-like strings and interpret timecode user input - * Default: 25 - */ - frameRate: number - - /** URL to endpoint where media preview are exposed */ - mediaPreviewsUrl: string // (former media_previews_url in config) - /** URLs for slack webhook to send evaluations */ - slackEvaluationUrls?: string // (former slack_evaluation in config) - - /** Media Resolutions supported by the studio for media playback */ - supportedMediaFormats?: string // (former mediaResolutions in config) - /** Audio Stream Formats supported by the studio for media playback */ - supportedAudioStreams?: string // (former audioStreams in config) - - /** Should the play from anywhere feature be enabled in this studio */ - enablePlayFromAnywhere?: boolean - - /** - * If set, forces the multi-playout-gateway mode (aka set "now"-time right away) - * for single playout-gateways setups - */ - forceMultiGatewayMode?: boolean - - /** How much extra delay to add to the Now-time (used for the "multi-playout-gateway" feature) . - * A higher value adds delays in playout, but reduces the risk of missed frames. */ - multiGatewayNowSafeLatency?: number - - /** Allow resets while a rundown is on-air */ - allowRundownResetOnAir?: boolean - - /** Preserve unsynced segments psoition in the rundown, relative to the other segments */ - preserveOrphanedSegmentPositionInRundown?: boolean - - /** - * The minimum amount of time, in milliseconds, that must pass after a take before another take may be performed. - * Default: 1000 - */ - minimumTakeSpan: number - - /** Whether to allow adlib testing mode, before a Part is playing in a Playlist */ - allowAdlibTestingSegment?: boolean - - /** Should QuickLoop context menu options be available to the users. It does not affect Playlist loop enabled by the NRCS. */ - enableQuickLoop?: boolean - - /** If and how to force auto-nexting in a looping Playlist */ - forceQuickLoopAutoNext?: ForceQuickLoopAutoNext - - /** - * The duration to apply on too short Parts Within QuickLoop when ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION is selected - * Default: 3000 - */ - fallbackPartDuration?: number - - /** - * Whether to allow hold operations for Rundowns in this Studio - * When disabled, any action-triggers that would normally trigger a hold operation will be silently ignored - * This should only block entering hold, to ensure Sofie doesn't get stuck if it somehow gets into hold - */ - allowHold: boolean - - /** - * Whether to allow direct playing of a piece in the rundown - * This behaviour is usally triggered by double-clicking on a piece in the GUI - */ - allowPieceDirectPlay: boolean - - /** - * Enable buckets - the default behavior is to have buckets. - */ - enableBuckets: boolean -} - export type StudioLight = Omit /** A set of available layer groups in a given installation */ @@ -140,7 +64,7 @@ export interface DBStudio { /** Config values are used by the Blueprints */ blueprintConfigWithOverrides: ObjectWithOverrides - settings: IStudioSettings + settingsWithOverrides: ObjectWithOverrides _rundownVersionHash: string diff --git a/packages/corelib/src/studio/baseline.ts b/packages/corelib/src/studio/baseline.ts index d1766e1245..86492cf75c 100644 --- a/packages/corelib/src/studio/baseline.ts +++ b/packages/corelib/src/studio/baseline.ts @@ -1,4 +1,4 @@ -import { StudioLight } from '../dataModel/Studio' +import { DBStudio } from '../dataModel/Studio' import { TimelineComplete } from '../dataModel/Timeline' import { ReadonlyDeep } from 'type-fest' import { unprotectString } from '../protectedString' @@ -6,7 +6,7 @@ import { Blueprint } from '../dataModel/Blueprint' export function shouldUpdateStudioBaselineInner( coreVersion: string, - studio: ReadonlyDeep, + studio: Pick, studioTimeline: ReadonlyDeep | null, studioBlueprint: Pick | null ): string | false { diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index 57a861bb9f..52df23225e 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -42,6 +42,7 @@ import { IDirectCollections } from '../db' import { ApmSpan, JobContext, + JobStudio, ProcessedShowStyleBase, ProcessedShowStyleCompound, ProcessedShowStyleVariant, @@ -56,6 +57,7 @@ import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlo import { removeRundownPlaylistFromDb } from '../ingest/__tests__/lib' import { processShowStyleBase, processShowStyleVariant } from '../jobs/showStyle' import { defaultStudio } from './defaultCollectionObjects' +import { convertStudioToJobStudio } from '../jobs/studio' export function setupDefaultJobEnvironment(studioId?: StudioId): MockJobContext { const { mockCollections, jobCollections } = getMockCollections() @@ -75,6 +77,7 @@ export class MockJobContext implements JobContext { #jobCollections: Readonly #mockCollections: Readonly #studio: ReadonlyDeep + #jobStudio: ReadonlyDeep #studioBlueprint: ReadonlyDeep #showStyleBlueprint: ReadonlyDeep @@ -87,6 +90,7 @@ export class MockJobContext implements JobContext { this.#jobCollections = jobCollections this.#mockCollections = mockCollections this.#studio = studio + this.#jobStudio = convertStudioToJobStudio(clone(studio)) this.#studioBlueprint = MockStudioBlueprint() this.#showStyleBlueprint = MockShowStyleBlueprint() @@ -103,7 +107,10 @@ export class MockJobContext implements JobContext { get studioId(): StudioId { return this.#studio._id } - get studio(): ReadonlyDeep { + get studio(): ReadonlyDeep { + return this.#jobStudio + } + get rawStudio(): ReadonlyDeep { return this.#studio } @@ -219,7 +226,7 @@ export class MockJobContext implements JobContext { } } getShowStyleBlueprintConfig(showStyle: ReadonlyDeep): ProcessedShowStyleConfig { - return preprocessShowStyleConfig(showStyle, this.#showStyleBlueprint, this.#studio.settings) + return preprocessShowStyleConfig(showStyle, this.#showStyleBlueprint, this.studio.settings) } hackPublishTimelineToFastTrack(_newTimeline: TimelineComplete): void { @@ -244,6 +251,7 @@ export class MockJobContext implements JobContext { setStudio(studio: ReadonlyDeep): void { this.#studio = clone(studio) + this.#jobStudio = convertStudioToJobStudio(clone(studio)) } setShowStyleBlueprint(blueprint: ReadonlyDeep): void { this.#showStyleBlueprint = blueprint @@ -288,7 +296,6 @@ const MockStudioBlueprint: () => StudioBlueprintManifest = () => ({ }, studioConfigSchema: JSONBlobStringify({}), - studioMigrations: [], getBaseline: () => { return { timelineObjects: [], @@ -320,7 +327,6 @@ const MockShowStyleBlueprint: () => ShowStyleBlueprintManifest = () => ({ }, showStyleConfigSchema: JSONBlobStringify({}), - showStyleMigrations: [], getShowStyleVariantId: (_context, variants): string | null => { return variants[0]._id }, diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 9ccd8fcf1a..77f56c6b60 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -102,7 +102,7 @@ export function defaultStudio(_id: StudioId): DBStudio { mappingsWithOverrides: wrapDefaultObject({}), supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, @@ -110,7 +110,7 @@ export function defaultStudio(_id: StudioId): DBStudio { allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), diff --git a/packages/job-worker/src/blueprints/__tests__/config.test.ts b/packages/job-worker/src/blueprints/__tests__/config.test.ts index c46b7a2fba..3e7e8bef50 100644 --- a/packages/job-worker/src/blueprints/__tests__/config.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/config.test.ts @@ -10,15 +10,15 @@ describe('Test blueprint config', () => { test('compileStudioConfig', () => { const jobContext = setupDefaultJobEnvironment() jobContext.setStudio({ - ...jobContext.studio, - settings: { + ...jobContext.rawStudio, + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }), }) jobContext.updateStudioBlueprint({ @@ -36,15 +36,15 @@ describe('Test blueprint config', () => { test('compileStudioConfig with function', () => { const jobContext = setupDefaultJobEnvironment() jobContext.setStudio({ - ...jobContext.studio, - settings: { + ...jobContext.rawStudio, + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }), }) jobContext.updateStudioBlueprint({ @@ -142,7 +142,7 @@ describe('Test blueprint config', () => { const studioId = jobContext.studioId jobContext.setStudio({ - ...jobContext.studio, + ...jobContext.rawStudio, blueprintConfigWithOverrides: wrapDefaultObject({ two: 'abc', number: 99, @@ -189,7 +189,7 @@ describe('Test blueprint config', () => { }, }) jobContext.setStudio({ - ...jobContext.studio, + ...jobContext.rawStudio, supportedShowStyleBase: [showStyle._id], }) jobContext.updateShowStyleBlueprint({ diff --git a/packages/job-worker/src/blueprints/__tests__/context.test.ts b/packages/job-worker/src/blueprints/__tests__/context.test.ts index 307289c2df..5388b21303 100644 --- a/packages/job-worker/src/blueprints/__tests__/context.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context.test.ts @@ -1,6 +1,5 @@ import { getHash } from '@sofie-automation/corelib/dist/lib' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' import { getShowStyleConfigRef, getStudioConfigRef } from '../configRefs' import { CommonContext } from '../context/CommonContext' @@ -81,7 +80,7 @@ describe('Test blueprint api context', () => { expect(context.studio).toBe(studio) expect(context.getStudioConfig()).toBe(studioConfig) - expect(context.getStudioMappings()).toEqual(applyAndValidateOverrides(studio.mappingsWithOverrides).obj) + expect(context.getStudioMappings()).toEqual(studio.mappings) }) test('getStudioConfigRef', () => { const context = new StudioContext( diff --git a/packages/job-worker/src/blueprints/__tests__/lib.ts b/packages/job-worker/src/blueprints/__tests__/lib.ts index cd34200204..29b54f4aac 100644 --- a/packages/job-worker/src/blueprints/__tests__/lib.ts +++ b/packages/job-worker/src/blueprints/__tests__/lib.ts @@ -18,7 +18,6 @@ export function generateFakeBlueprint( integrationVersion: '0.0.0', TSRVersion: '0.0.0', studioConfigManifest: [], - studioMigrations: [], getBaseline: () => { return { timelineObjects: [], @@ -45,8 +44,6 @@ export function generateFakeBlueprint( databaseVersion: { system: undefined, - showStyle: {}, - studio: {}, }, blueprintVersion: '', diff --git a/packages/job-worker/src/blueprints/config.ts b/packages/job-worker/src/blueprints/config.ts index 77ae34389d..78b755419f 100644 --- a/packages/job-worker/src/blueprints/config.ts +++ b/packages/job-worker/src/blueprints/config.ts @@ -11,10 +11,9 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyE import _ = require('underscore') import { logger } from '../logging' import { CommonContext } from './context' -import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { ProcessedShowStyleCompound, StudioCacheContext } from '../jobs' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { JobStudio, ProcessedShowStyleCompound, StudioCacheContext } from '../jobs' /** * Parse a string containing BlueprintConfigRefs (`${studio.studio0.myConfigField}`) to replace the refs with the current values @@ -100,10 +99,10 @@ export function compileCoreConfigValues(studioSettings: ReadonlyDeep, + studio: ReadonlyDeep, blueprint: ReadonlyDeep ): ProcessedStudioConfig { - let res: any = applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj + let res: any = studio.blueprintConfig try { if (blueprint.preprocessConfig) { diff --git a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts index 6c3f8cd30d..c71a4d33fe 100644 --- a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts @@ -4,7 +4,6 @@ import { ITimelineEventContext, } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { clone } from '@sofie-automation/corelib/dist/lib' @@ -14,7 +13,7 @@ import { getCurrentTime } from '../../lib' import { PieceInstance, ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' import _ = require('underscore') -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { convertPartInstanceToBlueprints, createBlueprintQuickLoopInfo } from './lib' import { RundownContext } from './RundownContext' import { AbSessionHelper } from '../../playout/abPlayback/abSessionHelper' @@ -33,7 +32,7 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli readonly #pieceInstanceCache = new Map>() constructor( - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/PartEventContext.ts b/packages/job-worker/src/blueprints/context/PartEventContext.ts index 880aa4b923..34722ec3ac 100644 --- a/packages/job-worker/src/blueprints/context/PartEventContext.ts +++ b/packages/job-worker/src/blueprints/context/PartEventContext.ts @@ -1,11 +1,10 @@ import { IBlueprintPartInstance, IPartEventContext } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { getCurrentTime } from '../../lib' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { convertPartInstanceToBlueprints } from './lib' import { RundownContext } from './RundownContext' @@ -14,7 +13,7 @@ export class PartEventContext extends RundownContext implements IPartEventContex constructor( eventName: string, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/RundownContext.ts b/packages/job-worker/src/blueprints/context/RundownContext.ts index 8faaefeba1..c84a27ac70 100644 --- a/packages/job-worker/src/blueprints/context/RundownContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownContext.ts @@ -1,10 +1,9 @@ import { IRundownContext, IBlueprintSegmentRundown } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { convertRundownToBlueprintSegmentRundown } from './lib' import { ContextInfo } from './CommonContext' import { ShowStyleContext } from './ShowStyleContext' @@ -19,7 +18,7 @@ export class RundownContext extends ShowStyleContext implements IRundownContext constructor( contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/RundownEventContext.ts b/packages/job-worker/src/blueprints/context/RundownEventContext.ts index b28f533063..9852e0dd72 100644 --- a/packages/job-worker/src/blueprints/context/RundownEventContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownEventContext.ts @@ -1,15 +1,14 @@ import { IEventContext } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { getCurrentTime } from '../../lib' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { RundownContext } from './RundownContext' export class RundownEventContext extends RundownContext implements IEventContext { constructor( - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/ShowStyleContext.ts b/packages/job-worker/src/blueprints/context/ShowStyleContext.ts index 7a243320b5..1310c72f72 100644 --- a/packages/job-worker/src/blueprints/context/ShowStyleContext.ts +++ b/packages/job-worker/src/blueprints/context/ShowStyleContext.ts @@ -1,9 +1,8 @@ import { IOutputLayer, IShowStyleContext, ISourceLayer } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' import { getShowStyleConfigRef } from '../configRefs' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { ContextInfo } from './CommonContext' import { StudioContext } from './StudioContext' @@ -12,7 +11,7 @@ import { StudioContext } from './StudioContext' export class ShowStyleContext extends StudioContext implements IShowStyleContext { constructor( contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, public readonly showStyleCompound: ReadonlyDeep, public readonly showStyleBlueprintConfig: ProcessedShowStyleConfig diff --git a/packages/job-worker/src/blueprints/context/StudioContext.ts b/packages/job-worker/src/blueprints/context/StudioContext.ts index f1627c483b..8d5915d338 100644 --- a/packages/job-worker/src/blueprints/context/StudioContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioContext.ts @@ -1,21 +1,18 @@ import { IStudioContext, BlueprintMappings } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio, MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProcessedStudioConfig } from '../config' import { getStudioConfigRef } from '../configRefs' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { CommonContext, ContextInfo } from './CommonContext' +import { JobStudio } from '../../jobs' /** Studio */ export class StudioContext extends CommonContext implements IStudioContext { - #processedMappings: ReadonlyDeep | undefined - constructor( contextInfo: ContextInfo, - public readonly studio: ReadonlyDeep, + public readonly studio: ReadonlyDeep, public readonly studioBlueprintConfig: ProcessedStudioConfig ) { super(contextInfo) @@ -36,10 +33,8 @@ export class StudioContext extends CommonContext implements IStudioContext { return getStudioConfigRef(this.studio._id, configKey) } getStudioMappings(): Readonly { - if (!this.#processedMappings) { - this.#processedMappings = applyAndValidateOverrides(this.studio.mappingsWithOverrides).obj - } + const mappings = this.studio.mappings // @ts-expect-error ProtectedString deviceId not compatible with string - return this.#processedMappings + return mappings } } diff --git a/packages/job-worker/src/blueprints/context/StudioUserContext.ts b/packages/job-worker/src/blueprints/context/StudioUserContext.ts index be2c471dc4..fff5232cd1 100644 --- a/packages/job-worker/src/blueprints/context/StudioUserContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioUserContext.ts @@ -1,17 +1,17 @@ import { IStudioUserContext, NoteSeverity } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ProcessedStudioConfig } from '../config' import { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' import { ContextInfo } from './CommonContext' import { StudioContext } from './StudioContext' +import { JobStudio } from '../../jobs' export class StudioUserContext extends StudioContext implements IStudioUserContext { public readonly notes: INoteBase[] = [] constructor( contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig ) { super(contextInfo, studio, studioBlueprintConfig) diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index 1515ae71ec..efef608cc8 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -25,8 +25,7 @@ import { convertPartialBlueprintMutablePartToCore, } from './lib' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { JobContext, ProcessedShowStyleCompound } from '../../jobs' +import { JobContext, JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { PieceTimelineObjectsBlob, serializePieceTimelineObjectsBlob, @@ -44,7 +43,7 @@ export class SyncIngestUpdateToPartInstanceContext constructor( private readonly _context: JobContext, contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep, partInstance: PlayoutPartInstanceModel, diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 444c8edc37..04d08f25c6 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -29,7 +29,6 @@ import { DatastorePersistenceMode } from '@sofie-automation/shared-lib/dist/core import { removeTimelineDatastoreValue, setTimelineDatastoreValue } from '../../playout/datastore' import { executePeripheralDeviceAction, listPlayoutDevices } from '../../peripheralDevice' import { ActionPartChange, PartAndPieceInstanceActionService } from './services/PartAndPieceInstanceActionService' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' import { setNextPartFromPart } from '../../playout/setNext' @@ -201,7 +200,8 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } async listRouteSets(): Promise> { - return applyAndValidateOverrides(this._context.studio.routeSetsWithOverrides).obj + // Discard ReadonlyDeep wrapper + return this._context.studio.routeSets as Record } async switchRouteSet(routeSetId: string, state: boolean | 'toggle'): Promise { diff --git a/packages/job-worker/src/blueprints/defaults/studio.ts b/packages/job-worker/src/blueprints/defaults/studio.ts index ff8a899cf8..43949c6384 100644 --- a/packages/job-worker/src/blueprints/defaults/studio.ts +++ b/packages/job-worker/src/blueprints/defaults/studio.ts @@ -26,7 +26,6 @@ export const DefaultStudioBlueprint: ReadonlyDeep = dee blueprintType: BlueprintManifestType.STUDIO, studioConfigSchema: JSONBlobStringify({}), - studioMigrations: [], configPresets: { 0: { diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index e46a0f827b..a9d96c0c97 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -47,6 +47,7 @@ import { UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanceModel' import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache' import { wrapGenericIngestJob, wrapGenericIngestJobWithPrecheck } from '../jobWrappers' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' const handleRemovedRundownWrapped = wrapGenericIngestJob(handleRemovedRundown) const handleUpdatedRundownWrapped = wrapGenericIngestJob(handleUpdatedRundown) @@ -121,11 +122,11 @@ describe('Test ingest actions for rundowns and segments', () => { const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, - settings: { + ...context.rawStudio, + settingsWithOverrides: wrapDefaultObject({ ...context.studio.settings, minimumTakeSpan: 0, - }, + }), supportedShowStyleBase: [showStyleCompound._id], }) diff --git a/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts index 2c6e7d8ce4..cb63251f09 100644 --- a/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts +++ b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts @@ -35,7 +35,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -57,7 +57,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [], }) @@ -76,7 +76,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -118,7 +118,7 @@ describe('selectShowStyleVariant', () => { const showStyleCompoundVariant2 = await setupMockShowStyleVariant(context, showStyleCompound._id) const showStyleCompound2 = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id, showStyleCompound2._id], }) @@ -153,7 +153,7 @@ describe('selectShowStyleVariant', () => { test('no show style bases', async () => { const context = setupDefaultJobEnvironment() context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [protectString('fakeId')], }) @@ -176,7 +176,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -201,7 +201,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -226,7 +226,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -251,7 +251,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) diff --git a/packages/job-worker/src/ingest/expectedPackages.ts b/packages/job-worker/src/ingest/expectedPackages.ts index c1d6099a9e..b94c6498b9 100644 --- a/packages/job-worker/src/ingest/expectedPackages.ts +++ b/packages/job-worker/src/ingest/expectedPackages.ts @@ -41,9 +41,8 @@ import { updateExpectedPlayoutItemsForPartModel, updateExpectedPlayoutItemsForRundownBaseline, } from './expectedPlayoutItems' -import { JobContext } from '../jobs' +import { JobContext, JobStudio } from '../jobs' import { ExpectedPackageForIngestModelBaseline, IngestModel } from './model/IngestModel' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { IngestPartModel } from './model/IngestPartModel' import { clone } from '@sofie-automation/corelib/dist/lib' @@ -160,7 +159,7 @@ export async function updateExpectedPackagesForRundownBaseline( } function generateExpectedPackagesForPiece( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, segmentId: SegmentId, pieces: ReadonlyDeep[], @@ -186,7 +185,7 @@ function generateExpectedPackagesForPiece( return packages } function generateExpectedPackagesForBaselineAdlibPiece( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, pieces: ReadonlyDeep ) { @@ -207,7 +206,7 @@ function generateExpectedPackagesForBaselineAdlibPiece( return packages } function generateExpectedPackagesForAdlibAction( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, segmentId: SegmentId, actions: ReadonlyDeep @@ -231,7 +230,7 @@ function generateExpectedPackagesForAdlibAction( return packages } function generateExpectedPackagesForBaselineAdlibAction( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, actions: ReadonlyDeep ) { @@ -251,7 +250,7 @@ function generateExpectedPackagesForBaselineAdlibAction( } return packages } -function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, adlibs: BucketAdLib[]) { +function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, adlibs: BucketAdLib[]) { const packages: ExpectedPackageDBFromBucketAdLib[] = [] for (const adlib of adlibs) { if (adlib.expectedPackages) { @@ -270,7 +269,7 @@ function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, return packages } function generateExpectedPackagesForBucketAdlibAction( - studio: ReadonlyDeep, + studio: ReadonlyDeep, adlibActions: BucketAdLibAction[] ) { const packages: ExpectedPackageDBFromBucketAdLibAction[] = [] @@ -291,7 +290,7 @@ function generateExpectedPackagesForBucketAdlibAction( return packages } function generateExpectedPackageBases( - studio: ReadonlyDeep, + studio: ReadonlyDeep, ownerId: | PieceId | AdLibActionId diff --git a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts index f251bf1586..9f1907ab44 100644 --- a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts +++ b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts @@ -87,7 +87,7 @@ describe('Test recieved mos ingest payloads', () => { const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) diff --git a/packages/job-worker/src/jobs/index.ts b/packages/job-worker/src/jobs/index.ts index f8ab90e9f4..586688a1f2 100644 --- a/packages/job-worker/src/jobs/index.ts +++ b/packages/job-worker/src/jobs/index.ts @@ -18,9 +18,11 @@ import { PlaylistLock, RundownLock } from './lock' import { BaseModel } from '../modelBase' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { ProcessedShowStyleBase, ProcessedShowStyleVariant, ProcessedShowStyleCompound } from './showStyle' +import { JobStudio } from './studio' export { ApmSpan } export { ProcessedShowStyleVariant, ProcessedShowStyleBase, ProcessedShowStyleCompound } +export { JobStudio } /** * Context for any job run in the job-worker @@ -101,10 +103,17 @@ export interface StudioCacheContext { * Id of the Studio the job belongs to */ readonly studioId: StudioId + /** + * The Studio the job belongs to. + * This has any ObjectWithOverrides in their computed/flattened form + */ + readonly studio: ReadonlyDeep + /** * The Studio the job belongs to + * This has any ObjectWithOverrides in their original form */ - readonly studio: ReadonlyDeep + readonly rawStudio: ReadonlyDeep /** * Blueprint for the studio the job belongs to diff --git a/packages/job-worker/src/jobs/studio.ts b/packages/job-worker/src/jobs/studio.ts new file mode 100644 index 0000000000..00a2f87893 --- /dev/null +++ b/packages/job-worker/src/jobs/studio.ts @@ -0,0 +1,58 @@ +import type { + IBlueprintConfig, + StudioRouteSet, + StudioRouteSetExclusivityGroup, +} from '@sofie-automation/blueprints-integration' +import type { DBStudio, IStudioSettings, MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { omit } from '@sofie-automation/corelib/dist/lib' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' + +/** + * A lightly processed version of DBStudio, with any ObjectWithOverrides pre-flattened + */ +export interface JobStudio + extends Omit< + DBStudio, + | 'mappingsWithOverrides' + | 'blueprintConfigWithOverrides' + | 'settingsWithOverrides' + | 'routeSetsWithOverrides' + | 'routeSetExclusivityGroupsWithOverrides' + | 'packageContainersWithOverrides' + > { + /** Mappings between the physical devices / outputs and logical ones */ + mappings: MappingsExt + + /** Config values are used by the Blueprints */ + blueprintConfig: IBlueprintConfig + + settings: IStudioSettings + + routeSets: Record + routeSetExclusivityGroups: Record + + // /** Contains settings for which Package Containers are present in the studio. + // * (These are used by the Package Manager and the Expected Packages) + // */ + // packageContainers: Record +} + +export function convertStudioToJobStudio(studio: DBStudio): JobStudio { + return { + ...omit( + studio, + 'mappingsWithOverrides', + 'blueprintConfigWithOverrides', + 'settingsWithOverrides', + 'routeSetsWithOverrides', + 'routeSetExclusivityGroupsWithOverrides', + 'packageContainersWithOverrides' + ), + mappings: applyAndValidateOverrides(studio.mappingsWithOverrides).obj, + blueprintConfig: applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj, + settings: applyAndValidateOverrides(studio.settingsWithOverrides).obj, + routeSets: applyAndValidateOverrides(studio.routeSetsWithOverrides).obj, + routeSetExclusivityGroups: applyAndValidateOverrides(studio.routeSetExclusivityGroupsWithOverrides).obj, + // packageContainers: applyAndValidateOverrides(studio.packageContainersWithOverrides).obj, + } +} diff --git a/packages/job-worker/src/playout/__tests__/playout.test.ts b/packages/job-worker/src/playout/__tests__/playout.test.ts index 01c63b2d17..93ab3ef753 100644 --- a/packages/job-worker/src/playout/__tests__/playout.test.ts +++ b/packages/job-worker/src/playout/__tests__/playout.test.ts @@ -47,6 +47,7 @@ import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheral import { ProcessedShowStyleCompound } from '../../jobs' import { handleOnPlayoutPlaybackChanged } from '../timings' import { sleep } from '@sofie-automation/shared-lib/dist/lib/lib' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' // const mockGetCurrentTime = jest.spyOn(lib, 'getCurrentTime') const mockExecutePeripheralDeviceFunction = jest @@ -98,11 +99,11 @@ describe('Playout API', () => { context = setupDefaultJobEnvironment() context.setStudio({ - ...context.studio, - settings: { + ...context.rawStudio, + settingsWithOverrides: wrapDefaultObject({ ...context.studio.settings, minimumTakeSpan: 0, - }, + }), }) // Ignore event jobs diff --git a/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts b/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts index bdf8a6dbec..f4efb9d410 100644 --- a/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts +++ b/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts @@ -8,7 +8,8 @@ import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/cont import { PlayoutSegmentModelImpl } from '../model/implementation/PlayoutSegmentModelImpl' import { PlayoutSegmentModel } from '../model/PlayoutSegmentModel' import { selectNextPart } from '../selectNextPart' -import { ForceQuickLoopAutoNext, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' class MockPart { constructor( diff --git a/packages/job-worker/src/playout/abPlayback/index.ts b/packages/job-worker/src/playout/abPlayback/index.ts index d803ecac46..6c2a1730c2 100644 --- a/packages/job-worker/src/playout/abPlayback/index.ts +++ b/packages/job-worker/src/playout/abPlayback/index.ts @@ -17,7 +17,6 @@ import { AbSessionHelper } from './abSessionHelper' import { ShowStyleContext } from '../../blueprints/context' import { logger } from '../../logging' import { ABPlayerDefinition } from '@sofie-automation/blueprints-integration' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { abPoolFilterDisabled, findPlayersInRouteSets } from './routeSetDisabling' /** @@ -73,7 +72,7 @@ export function applyAbPlaybackForTimeline( const now = getCurrentTime() const abConfiguration = blueprint.blueprint.getAbResolverConfiguration(blueprintContext) - const routeSetMembers = findPlayersInRouteSets(applyAndValidateOverrides(context.studio.routeSetsWithOverrides).obj) + const routeSetMembers = findPlayersInRouteSets(context.studio.routeSets) for (const [poolName, players] of Object.entries(abConfiguration.pools)) { // Filter out offline devices diff --git a/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts b/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts index d6be0a4ab6..9f71f1d0ac 100644 --- a/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts +++ b/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts @@ -1,6 +1,7 @@ import type { ABPlayerDefinition } from '@sofie-automation/blueprints-integration' import type { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' import { logger } from '../../logging' +import { ReadonlyDeep } from 'type-fest' /** * Map> @@ -8,9 +9,9 @@ import { logger } from '../../logging' */ type MembersOfRouteSets = Map> -export function findPlayersInRouteSets(routeSets: Record): MembersOfRouteSets { +export function findPlayersInRouteSets(routeSets: ReadonlyDeep>): MembersOfRouteSets { const routeSetEnabledPlayers: MembersOfRouteSets = new Map() - for (const [_key, routeSet] of Object.entries(routeSets)) { + for (const [_key, routeSet] of Object.entries>(routeSets)) { for (const abPlayer of routeSet.abPlayers) { let poolEntry = routeSetEnabledPlayers.get(abPlayer.poolName) if (!poolEntry) { diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index 0256447f51..1a925310bb 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -51,7 +51,7 @@ describe('Lookahead', () => { } } context.setStudio({ - ...context.studio, + ...context.rawStudio, mappingsWithOverrides: wrapDefaultObject(mappings), }) @@ -222,7 +222,7 @@ describe('Lookahead', () => { // Set really low { - const studio = clone(context.studio) + const studio = clone(context.rawStudio) studio.mappingsWithOverrides.defaults['WHEN_CLEAR'].lookaheadMaxSearchDistance = 0 studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = 0 context.setStudio(studio) @@ -236,7 +236,7 @@ describe('Lookahead', () => { // really high getOrderedPartsAfterPlayheadMock.mockClear() { - const studio = clone(context.studio) + const studio = clone(context.rawStudio) studio.mappingsWithOverrides.defaults['WHEN_CLEAR'].lookaheadMaxSearchDistance = -1 studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = 2000 context.setStudio(studio) @@ -250,7 +250,7 @@ describe('Lookahead', () => { // unset getOrderedPartsAfterPlayheadMock.mockClear() { - const studio = clone(context.studio) + const studio = clone(context.rawStudio) studio.mappingsWithOverrides.defaults['WHEN_CLEAR'].lookaheadMaxSearchDistance = undefined studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = -1 context.setStudio(studio) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts index 4c3cc76bd8..969506ce1a 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts @@ -11,7 +11,8 @@ import { defaultRundownPlaylist } from '../../../__mocks__/defaultCollectionObje import _ = require('underscore') import { wrapPartToTemporaryInstance } from '../../../__mocks__/partinstance' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ForceQuickLoopAutoNext, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' describe('getOrderedPartsAfterPlayhead', () => { let context!: MockJobContext @@ -37,7 +38,7 @@ describe('getOrderedPartsAfterPlayhead', () => { } } context.setStudio({ - ...context.studio, + ...context.rawStudio, mappingsWithOverrides: wrapDefaultObject(mappings), }) diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index 893d6eb174..12ac4935ec 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -22,7 +22,6 @@ import { LOOKAHEAD_DEFAULT_SEARCH_DISTANCE } from '@sofie-automation/shared-lib/ import { prefixSingleObjectId } from '../lib' import { LookaheadTimelineObject } from './findObjects' import { hasPieceInstanceDefinitelyEnded } from '../timeline/lib' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { ReadonlyDeep } from 'type-fest' @@ -65,8 +64,8 @@ export async function getLookeaheadObjects( partInstancesInfo0: SelectedPartInstancesTimelineInfo ): Promise> { const span = context.startSpan('getLookeaheadObjects') - const allMappings = applyAndValidateOverrides(context.studio.mappingsWithOverrides) - const mappingsToConsider = Object.entries(allMappings.obj).filter( + const allMappings = context.studio.mappings + const mappingsToConsider = Object.entries(allMappings).filter( ([_id, map]) => map.lookahead !== LookaheadMode.NONE && map.lookahead !== undefined ) if (mappingsToConsider.length === 0) { diff --git a/packages/job-worker/src/playout/model/services/QuickLoopService.ts b/packages/job-worker/src/playout/model/services/QuickLoopService.ts index eb7bb0c6e1..b9d252ca7a 100644 --- a/packages/job-worker/src/playout/model/services/QuickLoopService.ts +++ b/packages/job-worker/src/playout/model/services/QuickLoopService.ts @@ -1,11 +1,11 @@ import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/corelib/dist/playout/playlist' import { PlayoutModelReadonly } from '../PlayoutModel' import { - ForceQuickLoopAutoNext, QuickLoopMarker, QuickLoopMarkerType, QuickLoopProps, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/packages/job-worker/src/playout/selectNextPart.ts b/packages/job-worker/src/playout/selectNextPart.ts index 9b0ae14dd7..892c88aecf 100644 --- a/packages/job-worker/src/playout/selectNextPart.ts +++ b/packages/job-worker/src/playout/selectNextPart.ts @@ -1,11 +1,8 @@ import { DBPart, isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { JobContext } from '../jobs' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { - DBRundownPlaylist, - ForceQuickLoopAutoNext, - QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' import { PlayoutSegmentModel } from './model/PlayoutSegmentModel' diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 30011cd545..1a49c202a9 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -1,5 +1,5 @@ import { BlueprintId, TimelineHash } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { JobContext } from '../../jobs' +import { JobContext, JobStudio } from '../../jobs' import { ReadonlyDeep } from 'type-fest' import { BlueprintResultBaseline, @@ -36,7 +36,6 @@ import { WatchedPackagesHelper } from '../../blueprints/context/watchedPackages' import { postProcessStudioBaselineObjects } from '../../blueprints/postProcess' import { updateBaselineExpectedPackagesOnStudio } from '../../ingest/expectedPackages' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' -import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { convertResolvedPieceInstanceToBlueprints } from '../../blueprints/context/lib' import { buildTimelineObjsForRundown, RundownTimelineTimingContext } from './rundown' @@ -54,7 +53,7 @@ function isModelForStudio(model: StudioPlayoutModelBase): model is StudioPlayout } function generateTimelineVersions( - studio: ReadonlyDeep, + studio: ReadonlyDeep, blueprintId: BlueprintId | undefined, blueprintVersion: string ): TimelineCompleteGenerationVersions { diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index 935c9873b9..b6d2b43cb4 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -2,6 +2,7 @@ import { BlueprintMapping, BlueprintMappings, BlueprintParentDeviceSettings, + IStudioSettings, JSONBlobParse, StudioRouteBehavior, TSR, @@ -28,6 +29,7 @@ import { compileCoreConfigValues } from '../blueprints/config' import { CommonContext } from '../blueprints/context' import { JobContext } from '../jobs' import { FixUpBlueprintConfigContext } from '@sofie-automation/corelib/dist/fixUpBlueprintConfig/context' +import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' /** * Run the Blueprint applyConfig for the studio @@ -43,7 +45,7 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data name: 'applyConfig', identifier: `studio:${context.studioId},blueprint:${blueprint.blueprintId}`, }) - const rawBlueprintConfig = applyAndValidateOverrides(context.studio.blueprintConfigWithOverrides).obj + const rawBlueprintConfig = context.studio.blueprintConfig const result = blueprint.blueprint.applyConfig( blueprintContext, @@ -120,8 +122,18 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data ]) ) + const studioSettings: IStudioSettings = result.studioSettings ?? { + frameRate: 25, + mediaPreviewsUrl: '', + minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, + allowHold: true, + allowPieceDirectPlay: true, + enableBuckets: true, + } + await context.directCollections.Studios.update(context.studioId, { $set: { + 'settingsWithOverrides.defaults': studioSettings, 'mappingsWithOverrides.defaults': translateMappings(result.mappings), 'peripheralDeviceSettings.deviceSettings.defaults': parentDevices, 'peripheralDeviceSettings.playoutDevices.defaults': playoutDevices, @@ -170,7 +182,7 @@ export async function handleBlueprintValidateConfigForStudio( name: 'validateConfig', identifier: `studio:${context.studioId},blueprint:${blueprint.blueprintId}`, }) - const rawBlueprintConfig = applyAndValidateOverrides(context.studio.blueprintConfigWithOverrides).obj + const rawBlueprintConfig = applyAndValidateOverrides(context.rawStudio.blueprintConfigWithOverrides).obj // This clone seems excessive, but without it a DataCloneError is generated when posting the result to the parent const messages = clone(blueprint.blueprint.validateConfig(blueprintContext, rawBlueprintConfig)) @@ -212,7 +224,7 @@ export async function handleBlueprintFixUpConfigForStudio( const blueprintContext = new FixUpBlueprintConfigContext( commonContext, JSONBlobParse(blueprint.blueprint.studioConfigSchema), - context.studio.blueprintConfigWithOverrides + context.rawStudio.blueprintConfigWithOverrides ) blueprint.blueprint.fixUpConfig(blueprintContext) diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index c5efcae563..3bc545794b 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -1,10 +1,7 @@ import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { - DBRundownPlaylist, - ForceQuickLoopAutoNext, - QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { clone, getHash, @@ -27,12 +24,11 @@ import { IBlueprintRundown, NoteSeverity, } from '@sofie-automation/blueprints-integration' -import { JobContext } from './jobs' +import { JobContext, JobStudio } from './jobs' import { logger } from './logging' import { resetRundownPlaylist } from './playout/lib' import { runJobWithPlaylistLock, runWithPlayoutModel } from './playout/lock' import { updateTimeline } from './playout/timeline/generate' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { WrappedStudioBlueprint } from './blueprints/cache' import { StudioUserContext } from './blueprints/context' import { getCurrentTime } from './lib' @@ -315,7 +311,7 @@ export function produceRundownPlaylistInfoFromRundown( function defaultPlaylistForRundown( rundown: ReadonlyDeep, - studio: ReadonlyDeep, + studio: ReadonlyDeep, existingPlaylist?: ReadonlyDeep ): Omit { return { diff --git a/packages/job-worker/src/workers/caches.ts b/packages/job-worker/src/workers/caches.ts index f252a9a0de..80d7eec6f1 100644 --- a/packages/job-worker/src/workers/caches.ts +++ b/packages/job-worker/src/workers/caches.ts @@ -15,8 +15,9 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { clone, deepFreeze } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging' import deepmerge = require('deepmerge') -import { ProcessedShowStyleBase, ProcessedShowStyleVariant, StudioCacheContext } from '../jobs' +import { JobStudio, ProcessedShowStyleBase, ProcessedShowStyleVariant, StudioCacheContext } from '../jobs' import { StudioCacheContextImpl } from './context/StudioCacheContextImpl' +import { convertStudioToJobStudio } from '../jobs/studio' /** * A Wrapper to maintain a cache and provide a context using the cache when appropriate @@ -43,7 +44,7 @@ export class WorkerDataCacheWrapperImpl implements WorkerDataCacheWrapper { * The StudioId the cache is maintained for */ get studioId(): StudioId { - return this.#dataCache.studio._id + return this.#dataCache.rawStudio._id } constructor(collections: IDirectCollections, dataCache: WorkerDataCache) { @@ -99,7 +100,16 @@ export class WorkerDataCacheWrapperImpl implements WorkerDataCacheWrapper { * This is a reusable cache of these properties */ export interface WorkerDataCache { - studio: ReadonlyDeep + /** + * The Studio the cache belongs to + * This has any ObjectWithOverrides in their original form + */ + rawStudio: ReadonlyDeep + /** + * The Studio the cache belongs to. + * This has any ObjectWithOverrides in their computed/flattened form + */ + jobStudio: ReadonlyDeep studioBlueprint: ReadonlyDeep studioBlueprintConfig: ProcessedStudioConfig | undefined @@ -133,12 +143,16 @@ export async function loadWorkerDataCache( studioId: StudioId ): Promise { // Load some 'static' data from the db - const studio = deepFreeze(await collections.Studios.findOne(studioId)) - if (!studio) throw new Error('Missing studio') + const dbStudio = await collections.Studios.findOne(studioId) + if (!dbStudio) throw new Error('Missing studio') + const studio = deepFreeze(dbStudio) const studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, studio) + const jobStudio = deepFreeze(convertStudioToJobStudio(dbStudio)) + return { - studio, + rawStudio: studio, + jobStudio: jobStudio, studioBlueprint, studioBlueprintConfig: undefined, @@ -157,11 +171,12 @@ export async function invalidateWorkerDataCache( if (data.forceAll) { // Clear everything! - const newStudio = await collections.Studios.findOne(cache.studio._id) + const newStudio = await collections.Studios.findOne(cache.rawStudio._id) if (!newStudio) throw new Error(`Studio is missing during cache invalidation!`) - cache.studio = deepFreeze(newStudio) + cache.rawStudio = deepFreeze(newStudio) + cache.jobStudio = deepFreeze(convertStudioToJobStudio(newStudio)) - cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio) + cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.rawStudio) cache.studioBlueprintConfig = undefined cache.showStyleBases.clear() @@ -176,25 +191,26 @@ export async function invalidateWorkerDataCache( if (data.studio) { logger.debug('WorkerDataCache: Reloading studio') - const newStudio = await collections.Studios.findOne(cache.studio._id) + const newStudio = await collections.Studios.findOne(cache.rawStudio._id) if (!newStudio) throw new Error(`Studio is missing during cache invalidation!`) // If studio blueprintId changed, then force it to be reloaded - if (newStudio.blueprintId !== cache.studio.blueprintId) updateStudioBlueprint = true + if (newStudio.blueprintId !== cache.rawStudio.blueprintId) updateStudioBlueprint = true - cache.studio = deepFreeze(newStudio) + cache.rawStudio = deepFreeze(newStudio) + cache.jobStudio = deepFreeze(convertStudioToJobStudio(newStudio)) cache.studioBlueprintConfig = undefined } // Check if studio blueprint was in the changed list - if (!updateStudioBlueprint && cache.studio.blueprintId) { - updateStudioBlueprint = data.blueprints.includes(cache.studio.blueprintId) + if (!updateStudioBlueprint && cache.rawStudio.blueprintId) { + updateStudioBlueprint = data.blueprints.includes(cache.rawStudio.blueprintId) } // Reload studioBlueprint if (updateStudioBlueprint) { logger.debug('WorkerDataCache: Reloading studioBlueprint') - cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio) + cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.rawStudio) cache.studioBlueprintConfig = undefined } @@ -210,7 +226,7 @@ export async function invalidateWorkerDataCache( if (data.studio) { // Ensure showStyleBases & showStyleVariants are all still valid for the studio - const allowedBases = new Set(cache.studio.supportedShowStyleBase) + const allowedBases = new Set(cache.rawStudio.supportedShowStyleBase) for (const id of Array.from(cache.showStyleBases.keys())) { if (!allowedBases.has(id)) { diff --git a/packages/job-worker/src/workers/context/JobContextImpl.ts b/packages/job-worker/src/workers/context/JobContextImpl.ts index 7be35b55f2..8cd15572dc 100644 --- a/packages/job-worker/src/workers/context/JobContextImpl.ts +++ b/packages/job-worker/src/workers/context/JobContextImpl.ts @@ -1,5 +1,5 @@ import { IDirectCollections } from '../../db' -import { JobContext } from '../../jobs' +import { JobContext, JobStudio } from '../../jobs' import { WorkerDataCache } from '../caches' import { RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { getIngestQueueName, IngestJobFunc } from '@sofie-automation/corelib/dist/worker/ingest' @@ -41,8 +41,12 @@ export class JobContextImpl extends StudioCacheContextImpl implements JobContext this.studioRouteSetUpdater = new StudioRouteSetUpdater(directCollections, cacheData) } - get studio(): ReadonlyDeep { - return this.studioRouteSetUpdater.studioWithChanges ?? super.studio + get studio(): ReadonlyDeep { + return this.studioRouteSetUpdater.jobStudioWithChanges ?? super.studio + } + + get rawStudio(): ReadonlyDeep { + return this.studioRouteSetUpdater.rawStudioWithChanges ?? super.rawStudio } trackCache(cache: BaseModel): void { diff --git a/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts b/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts index dff38b6e88..183a89d01b 100644 --- a/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts +++ b/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts @@ -4,6 +4,7 @@ import { ProcessedShowStyleVariant, ProcessedShowStyleCompound, StudioCacheContext, + JobStudio, } from '../../jobs' import { ReadonlyDeep } from 'type-fest' import { WorkerDataCache } from '../caches' @@ -30,9 +31,14 @@ export class StudioCacheContextImpl implements StudioCacheContext { protected readonly cacheData: WorkerDataCache ) {} - get studio(): ReadonlyDeep { + get studio(): ReadonlyDeep { // This is frozen at the point of populating the cache - return this.cacheData.studio + return this.cacheData.jobStudio + } + + get rawStudio(): ReadonlyDeep { + // This is frozen at the point of populating the cache + return this.cacheData.rawStudio } get studioId(): StudioId { @@ -47,7 +53,9 @@ export class StudioCacheContextImpl implements StudioCacheContext { getStudioBlueprintConfig(): ProcessedStudioConfig { if (!this.cacheData.studioBlueprintConfig) { this.cacheData.studioBlueprintConfig = deepFreeze( - clone(preprocessStudioConfig(this.cacheData.studio, this.cacheData.studioBlueprint.blueprint) ?? null) + clone( + preprocessStudioConfig(this.cacheData.jobStudio, this.cacheData.studioBlueprint.blueprint) ?? null + ) ) } @@ -59,7 +67,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { const loadedDocs: Array> = [] // Figure out what is already cached, and what needs loading - for (const id of this.cacheData.studio.supportedShowStyleBase) { + for (const id of this.cacheData.jobStudio.supportedShowStyleBase) { const doc = this.cacheData.showStyleBases.get(id) if (doc === undefined) { docsToLoad.push(id) @@ -95,7 +103,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { async getShowStyleBase(id: ShowStyleBaseId): Promise> { // Check if allowed - if (!this.cacheData.studio.supportedShowStyleBase.includes(id)) { + if (!this.cacheData.jobStudio.supportedShowStyleBase.includes(id)) { throw new Error(`ShowStyleBase "${id}" is not allowed in studio`) } @@ -123,7 +131,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { async getShowStyleVariants(id: ShowStyleBaseId): Promise>> { // Check if allowed - if (!this.cacheData.studio.supportedShowStyleBase.includes(id)) { + if (!this.cacheData.jobStudio.supportedShowStyleBase.includes(id)) { throw new Error(`ShowStyleBase "${id}" is not allowed in studio`) } @@ -172,7 +180,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { const doc0 = await this.directCollections.ShowStyleVariants.findOne(id) // Check allowed - if (doc0 && !this.cacheData.studio.supportedShowStyleBase.includes(doc0.showStyleBaseId)) { + if (doc0 && !this.cacheData.jobStudio.supportedShowStyleBase.includes(doc0.showStyleBaseId)) { throw new Error(`ShowStyleVariant "${id}" is not allowed in studio`) } @@ -187,7 +195,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { if (doc) { // Check allowed - if (!this.cacheData.studio.supportedShowStyleBase.includes(doc.showStyleBaseId)) { + if (!this.cacheData.jobStudio.supportedShowStyleBase.includes(doc.showStyleBaseId)) { throw new Error(`ShowStyleVariant "${id}" is not allowed in studio`) } diff --git a/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts b/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts index cea5c9e53b..9de2f486f4 100644 --- a/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts +++ b/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts @@ -10,26 +10,39 @@ import { logger } from '../../logging' import type { ReadonlyDeep } from 'type-fest' import type { WorkerDataCache } from '../caches' import type { IDirectCollections } from '../../db' +import { JobStudio } from '../../jobs' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' export class StudioRouteSetUpdater { readonly #directCollections: Readonly - readonly #cacheData: Pick + readonly #cacheData: Pick - constructor(directCollections: Readonly, cacheData: Pick) { + constructor( + directCollections: Readonly, + cacheData: Pick + ) { this.#directCollections = directCollections this.#cacheData = cacheData } // Future: this could store a Map, if the context exposed a simplified view of DBStudio - #studioWithRouteSetChanges: ReadonlyDeep | undefined = undefined - - get studioWithChanges(): ReadonlyDeep | undefined { - return this.#studioWithRouteSetChanges + #studioWithRouteSetChanges: + | { + rawStudio: ReadonlyDeep + jobStudio: ReadonlyDeep + } + | undefined = undefined + + get rawStudioWithChanges(): ReadonlyDeep | undefined { + return this.#studioWithRouteSetChanges?.rawStudio + } + get jobStudioWithChanges(): ReadonlyDeep | undefined { + return this.#studioWithRouteSetChanges?.jobStudio } setRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean { - const currentStudio = this.#studioWithRouteSetChanges ?? this.#cacheData.studio - const currentRouteSets = getAllCurrentItemsFromOverrides(currentStudio.routeSetsWithOverrides, null) + const currentStudios = this.#studioWithRouteSetChanges ?? this.#cacheData + const currentRouteSets = getAllCurrentItemsFromOverrides(currentStudios.rawStudio.routeSetsWithOverrides, null) const routeSet = currentRouteSets.find((routeSet) => routeSet.id === routeSetId) if (!routeSet) throw new Error(`RouteSet "${routeSetId}" not found!`) @@ -41,10 +54,10 @@ export class StudioRouteSetUpdater { if (routeSet.computed.behavior === StudioRouteBehavior.ACTIVATE_ONLY && !isActive) throw new Error(`RouteSet "${routeSet.id}" is ACTIVATE_ONLY`) - const overrideHelper = new OverrideOpHelperImpl(null, currentStudio.routeSetsWithOverrides) + const overrideHelper = new OverrideOpHelperImpl(null, currentStudios.rawStudio.routeSetsWithOverrides) // Update the pending changes - logger.debug(`switchRouteSet "${this.#cacheData.studio._id}" "${routeSet.id}"=${isActive}`) + logger.debug(`switchRouteSet "${this.#cacheData.rawStudio._id}" "${routeSet.id}"=${isActive}`) overrideHelper.setItemValue(routeSetId, 'active', isActive) let mayAffectTimeline = couldRoutesetAffectTimelineGeneration(routeSet) @@ -54,7 +67,9 @@ export class StudioRouteSetUpdater { for (const otherRouteSet of Object.values>(currentRouteSets)) { if (otherRouteSet.id === routeSet.id) continue if (otherRouteSet.computed?.exclusivityGroup === routeSet.computed.exclusivityGroup) { - logger.debug(`switchRouteSet Other ID "${this.#cacheData.studio._id}" "${otherRouteSet.id}"=false`) + logger.debug( + `switchRouteSet Other ID "${this.#cacheData.rawStudio._id}" "${otherRouteSet.id}"=false` + ) overrideHelper.setItemValue(otherRouteSet.id, 'active', false) mayAffectTimeline = mayAffectTimeline || couldRoutesetAffectTimelineGeneration(otherRouteSet) @@ -65,13 +80,22 @@ export class StudioRouteSetUpdater { const updatedOverrideOps = overrideHelper.getPendingOps() // Update the cached studio - this.#studioWithRouteSetChanges = Object.freeze({ - ...currentStudio, + const updatedRawStudio: ReadonlyDeep = Object.freeze({ + ...currentStudios.rawStudio, routeSetsWithOverrides: Object.freeze({ - ...currentStudio.routeSetsWithOverrides, + ...currentStudios.rawStudio.routeSetsWithOverrides, overrides: deepFreeze(updatedOverrideOps), }), }) + const updatedJobStudio: ReadonlyDeep = Object.freeze({ + ...currentStudios.jobStudio, + routeSets: deepFreeze(applyAndValidateOverrides(updatedRawStudio.routeSetsWithOverrides).obj), + }) + + this.#studioWithRouteSetChanges = { + rawStudio: updatedRawStudio, + jobStudio: updatedJobStudio, + } return mayAffectTimeline } @@ -83,18 +107,19 @@ export class StudioRouteSetUpdater { // This is technically a little bit of a race condition, if someone uses the config pages but no more so than the rest of the system await this.#directCollections.Studios.update( { - _id: this.#cacheData.studio._id, + _id: this.#cacheData.rawStudio._id, }, { $set: { 'routeSetsWithOverrides.overrides': - this.#studioWithRouteSetChanges.routeSetsWithOverrides.overrides, + this.#studioWithRouteSetChanges.rawStudio.routeSetsWithOverrides.overrides, }, } ) // Pretend that the studio as reported by the database has changed, this will be fixed after this job by the ChangeStream firing - this.#cacheData.studio = this.#studioWithRouteSetChanges + this.#cacheData.rawStudio = this.#studioWithRouteSetChanges.rawStudio + this.#cacheData.jobStudio = this.#studioWithRouteSetChanges.jobStudio this.#studioWithRouteSetChanges = undefined } diff --git a/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts b/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts index 77692f4072..e6ef0fe40d 100644 --- a/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts +++ b/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts @@ -6,11 +6,15 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec function setupTest(routeSets: Record) { const context = setupDefaultJobEnvironment() - const mockCache: Pick = { - studio: { - ...context.studio, + const mockCache: Pick = { + rawStudio: { + ...context.rawStudio, routeSetsWithOverrides: wrapDefaultObject(routeSets), }, + jobStudio: { + ...context.studio, + routeSets: routeSets, + }, } const mockCollection = context.mockCollections.Studios const routeSetHelper = new StudioRouteSetUpdater(context.directCollections, mockCache) @@ -197,11 +201,13 @@ describe('StudioRouteSetUpdater', () => { routeSetHelper.setRouteSetActive('one', true) - expect(routeSetHelper.studioWithChanges).toBeTruthy() + expect(routeSetHelper.rawStudioWithChanges).toBeTruthy() + expect(routeSetHelper.jobStudioWithChanges).toBeTruthy() routeSetHelper.discardRouteSetChanges() - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() @@ -211,54 +217,70 @@ describe('StudioRouteSetUpdater', () => { it('save should update mockCache', async () => { const { mockCache, mockCollection, routeSetHelper } = setupTest(SINGLE_ROUTESET) - const studioBefore = mockCache.studio - expect(routeSetHelper.studioWithChanges).toBeFalsy() + const rawStudioBefore = mockCache.rawStudio + const jobStudioBefore = mockCache.jobStudio + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() routeSetHelper.setRouteSetActive('one', true) - expect(routeSetHelper.studioWithChanges).toBeTruthy() + expect(routeSetHelper.rawStudioWithChanges).toBeTruthy() + expect(routeSetHelper.jobStudioWithChanges).toBeTruthy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() expect(mockCollection.operations).toHaveLength(1) // Object should have changed - expect(mockCache.studio).not.toBe(studioBefore) + expect(mockCache.rawStudio).not.toBe(rawStudioBefore) + expect(mockCache.jobStudio).not.toBe(jobStudioBefore) // Object should not be equal - expect(mockCache.studio).not.toEqual(studioBefore) - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(mockCache.rawStudio).not.toEqual(rawStudioBefore) + expect(mockCache.jobStudio).not.toEqual(jobStudioBefore) + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() }) it('no changes should not update mockCache', async () => { const { mockCache, mockCollection, routeSetHelper } = setupTest(SINGLE_ROUTESET) - const studioBefore = mockCache.studio - expect(routeSetHelper.studioWithChanges).toBeFalsy() + const rawStudioBefore = mockCache.rawStudio + const jobStudioBefore = mockCache.jobStudio + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() expect(mockCollection.operations).toHaveLength(0) - expect(mockCache.studio).toBe(studioBefore) - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(mockCache.rawStudio).toBe(rawStudioBefore) + expect(mockCache.jobStudio).toBe(jobStudioBefore) + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() }) it('discard changes should not update mockCache', async () => { const { mockCache, mockCollection, routeSetHelper } = setupTest(SINGLE_ROUTESET) - const studioBefore = mockCache.studio - expect(routeSetHelper.studioWithChanges).toBeFalsy() + const rawStudioBefore = mockCache.rawStudio + const jobStudioBefore = mockCache.jobStudio + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() routeSetHelper.setRouteSetActive('one', true) - expect(routeSetHelper.studioWithChanges).toBeTruthy() + expect(routeSetHelper.rawStudioWithChanges).toBeTruthy() + expect(routeSetHelper.jobStudioWithChanges).toBeTruthy() routeSetHelper.discardRouteSetChanges() - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() expect(mockCollection.operations).toHaveLength(0) - expect(mockCache.studio).toBe(studioBefore) - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(mockCache.rawStudio).toBe(rawStudioBefore) + expect(mockCache.jobStudio).toBe(jobStudioBefore) + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() }) it('ACTIVATE_ONLY routeset can be activated', async () => { diff --git a/packages/job-worker/src/workers/events/child.ts b/packages/job-worker/src/workers/events/child.ts index 76d95c4c31..d6c0a59da1 100644 --- a/packages/job-worker/src/workers/events/child.ts +++ b/packages/job-worker/src/workers/events/child.ts @@ -98,7 +98,7 @@ export class EventsWorkerChild { const transaction = startTransaction('invalidateCaches', 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } try { @@ -118,7 +118,7 @@ export class EventsWorkerChild { const trace = startTrace('studioWorker' + jobName) const transaction = startTransaction(jobName, 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } const context = new JobContextImpl( diff --git a/packages/job-worker/src/workers/ingest/child.ts b/packages/job-worker/src/workers/ingest/child.ts index 86af4b8634..41ebc90eaf 100644 --- a/packages/job-worker/src/workers/ingest/child.ts +++ b/packages/job-worker/src/workers/ingest/child.ts @@ -81,7 +81,7 @@ export class IngestWorkerChild { const transaction = startTransaction('invalidateCaches', 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } try { @@ -99,7 +99,7 @@ export class IngestWorkerChild { const trace = startTrace('ingestWorker:' + jobName) const transaction = startTransaction(jobName, 'worker-ingest') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) // transaction.setLabel('rundownId', unprotectString(staticData.rundownId)) } diff --git a/packages/job-worker/src/workers/studio/child.ts b/packages/job-worker/src/workers/studio/child.ts index 40903527c6..cd468780ee 100644 --- a/packages/job-worker/src/workers/studio/child.ts +++ b/packages/job-worker/src/workers/studio/child.ts @@ -82,7 +82,7 @@ export class StudioWorkerChild { const transaction = startTransaction('invalidateCaches', 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } try { @@ -100,7 +100,7 @@ export class StudioWorkerChild { const trace = startTrace('studioWorker:' + jobName) const transaction = startTransaction(jobName, 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } const context = new JobContextImpl( diff --git a/packages/meteor-lib/src/api/migration.ts b/packages/meteor-lib/src/api/migration.ts index 40df1b77a0..80f6485667 100644 --- a/packages/meteor-lib/src/api/migration.ts +++ b/packages/meteor-lib/src/api/migration.ts @@ -1,5 +1,11 @@ import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' -import { BlueprintId, ShowStyleBaseId, SnapshotId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + BlueprintId, + CoreSystemId, + ShowStyleBaseId, + SnapshotId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' @@ -64,10 +70,15 @@ export interface NewMigrationAPI { validateConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise /** - * Run `applyConfig` on the blueprint for a Studio, and store the results into the db - * @param studioId Id of the Studio + * Run `applyConfig` on the blueprint for a ShowStyleBase, and store the results into the db + * @param showStyleBaseId Id of the ShowStyleBase */ runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise + + /** + * Run `applyConfig` on the blueprint for the CoreSystem, and store the results into the db + */ + runUpgradeForCoreSystem(coreSystemId: CoreSystemId): Promise } export enum MigrationAPIMethods { @@ -85,6 +96,7 @@ export enum MigrationAPIMethods { 'ignoreFixupConfigForShowStyleBase' = 'migration.ignoreFixupConfigForShowStyleBase', 'validateConfigForShowStyleBase' = 'migration.validateConfigForShowStyleBase', 'runUpgradeForShowStyleBase' = 'migration.runUpgradeForShowStyleBase', + 'runUpgradeForCoreSystem' = 'migration.runUpgradeForCoreSystem', } export interface GetMigrationStatusResult { diff --git a/packages/meteor-lib/src/api/upgradeStatus.ts b/packages/meteor-lib/src/api/upgradeStatus.ts index d50fdd81e1..2f6d025d3f 100644 --- a/packages/meteor-lib/src/api/upgradeStatus.ts +++ b/packages/meteor-lib/src/api/upgradeStatus.ts @@ -1,16 +1,19 @@ import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' -import { StudioId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { StudioId, ShowStyleBaseId, CoreSystemId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' export type UIBlueprintUpgradeStatusId = ProtectedString<'UIBlueprintUpgradeStatus'> -export type UIBlueprintUpgradeStatus = UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle +export type UIBlueprintUpgradeStatus = + | UIBlueprintUpgradeStatusCoreSystem + | UIBlueprintUpgradeStatusStudio + | UIBlueprintUpgradeStatusShowStyle export interface UIBlueprintUpgradeStatusBase { _id: UIBlueprintUpgradeStatusId - documentType: 'studio' | 'showStyle' - documentId: StudioId | ShowStyleBaseId + documentType: 'coreSystem' | 'studio' | 'showStyle' + documentId: CoreSystemId | StudioId | ShowStyleBaseId name: string @@ -30,6 +33,11 @@ export interface UIBlueprintUpgradeStatusBase { changes: ITranslatableMessage[] } +export interface UIBlueprintUpgradeStatusCoreSystem extends UIBlueprintUpgradeStatusBase { + documentType: 'coreSystem' + documentId: CoreSystemId +} + export interface UIBlueprintUpgradeStatusStudio extends UIBlueprintUpgradeStatusBase { documentType: 'studio' documentId: StudioId diff --git a/packages/meteor-lib/src/collections/CoreSystem.ts b/packages/meteor-lib/src/collections/CoreSystem.ts index 5a8f58641b..e710091a16 100644 --- a/packages/meteor-lib/src/collections/CoreSystem.ts +++ b/packages/meteor-lib/src/collections/CoreSystem.ts @@ -1,6 +1,9 @@ +import { LastBlueprintConfig } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { LogLevel } from '../lib' import { CoreSystemId, BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { ObjectWithOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' export const SYSTEM_ID: CoreSystemId = protectString('core') @@ -54,22 +57,11 @@ export interface ICoreSystem { /** Id of the blueprint used by this system */ blueprintId?: BlueprintId - /** Support info */ - support?: { - message: string - } - systemInfo?: { message: string enabled: boolean } - evaluations?: { - enabled: boolean - heading: string - message: string - } - /** A user-defined name for the installation */ name?: string @@ -95,18 +87,20 @@ export interface ICoreSystem { } enableMonitorBlockedThread?: boolean - /** Cron jobs running nightly */ - cron?: { - casparCGRestart?: { - enabled: boolean - } - storeRundownSnapshots?: { - enabled: boolean - rundownNames?: string[] - } - } + settingsWithOverrides: ObjectWithOverrides logo?: SofieLogo + + /** Details on the last blueprint used to generate the defaults values for this + * Note: This doesn't currently have any 'config' which it relates to. + * The name is to be consistent with studio/showstyle, and in preparation for their being config/configpresets used here + */ + lastBlueprintConfig: LastBlueprintConfig | undefined + + /** These fields are to have type consistency with the full config driven upgrades flow, but we don't use them yet */ + blueprintConfigPresetId?: undefined + lastBlueprintFixUpHash?: undefined + blueprintConfigWithOverrides?: undefined } /** In the beginning, there was the database, and the database was with Sofie, and the database was Sofie. diff --git a/packages/shared-lib/src/core/model/CoreSystemSettings.ts b/packages/shared-lib/src/core/model/CoreSystemSettings.ts new file mode 100644 index 0000000000..e5392915a5 --- /dev/null +++ b/packages/shared-lib/src/core/model/CoreSystemSettings.ts @@ -0,0 +1,23 @@ +export interface ICoreSystemSettings { + /** Cron jobs running nightly */ + cron: { + casparCGRestart: { + enabled: boolean + } + storeRundownSnapshots?: { + enabled: boolean + rundownNames?: string[] + } + } + + /** Support info */ + support: { + message: string + } + + evaluationsMessage: { + enabled: boolean + heading: string + message: string + } +} diff --git a/packages/shared-lib/src/core/model/StudioSettings.ts b/packages/shared-lib/src/core/model/StudioSettings.ts new file mode 100644 index 0000000000..f964362679 --- /dev/null +++ b/packages/shared-lib/src/core/model/StudioSettings.ts @@ -0,0 +1,84 @@ +export enum ForceQuickLoopAutoNext { + /** Parts will auto-next only when explicitly set by the NRCS/blueprints */ + DISABLED = 'disabled', + /** Parts will auto-next when the expected duration is set and within range */ + ENABLED_WHEN_VALID_DURATION = 'enabled_when_valid_duration', + /** All parts will auto-next. If expected duration is undefined or low, the default display duration will be used */ + ENABLED_FORCING_MIN_DURATION = 'enabled_forcing_min_duration', +} + +export interface IStudioSettings { + /** The framerate (frames per second) used to convert internal timing information (in milliseconds) + * into timecodes and timecode-like strings and interpret timecode user input + * Default: 25 + */ + frameRate: number + + /** URL to endpoint where media preview are exposed */ + mediaPreviewsUrl: string // (former media_previews_url in config) + /** URLs for slack webhook to send evaluations */ + slackEvaluationUrls?: string // (former slack_evaluation in config) + + /** Media Resolutions supported by the studio for media playback */ + supportedMediaFormats?: string // (former mediaResolutions in config) + /** Audio Stream Formats supported by the studio for media playback */ + supportedAudioStreams?: string // (former audioStreams in config) + + /** Should the play from anywhere feature be enabled in this studio */ + enablePlayFromAnywhere?: boolean + + /** + * If set, forces the multi-playout-gateway mode (aka set "now"-time right away) + * for single playout-gateways setups + */ + forceMultiGatewayMode?: boolean + + /** How much extra delay to add to the Now-time (used for the "multi-playout-gateway" feature). + * A higher value adds delays in playout, but reduces the risk of missed frames. */ + multiGatewayNowSafeLatency?: number + + /** Allow resets while a rundown is on-air */ + allowRundownResetOnAir?: boolean + + /** Preserve unsynced segments position in the rundown, relative to the other segments */ + preserveOrphanedSegmentPositionInRundown?: boolean + + /** + * The minimum amount of time, in milliseconds, that must pass after a take before another take may be performed. + * Default: 1000 + */ + minimumTakeSpan: number + + /** Whether to allow adlib testing mode, before a Part is playing in a Playlist */ + allowAdlibTestingSegment?: boolean + + /** Should QuickLoop context menu options be available to the users. It does not affect Playlist loop enabled by the NRCS. */ + enableQuickLoop?: boolean + + /** If and how to force auto-nexting in a looping Playlist */ + forceQuickLoopAutoNext?: ForceQuickLoopAutoNext + + /** + * The duration to apply on too short Parts Within QuickLoop when ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION is selected + * Default: 3000 + */ + fallbackPartDuration?: number + + /** + * Whether to allow hold operations for Rundowns in this Studio + * When disabled, any action-triggers that would normally trigger a hold operation will be silently ignored + * This should only block entering hold, to ensure Sofie doesn't get stuck if it somehow gets into hold + */ + allowHold: boolean + + /** + * Whether to allow direct playing of a piece in the rundown + * This behaviour is usally triggered by double-clicking on a piece in the GUI + */ + allowPieceDirectPlay: boolean + + /** + * Enable buckets - the default behavior is to have buckets. + */ + enableBuckets: boolean +} diff --git a/packages/webui/public/images/sofie-logo.svg b/packages/webui/public/images/sofie-logo-default.svg similarity index 100% rename from packages/webui/public/images/sofie-logo.svg rename to packages/webui/public/images/sofie-logo-default.svg diff --git a/packages/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index a21796d77f..a17d5e18fb 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -101,14 +101,14 @@ export function defaultStudio(_id: StudioId): DBStudio { mappingsWithOverrides: wrapDefaultObject({}), supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), diff --git a/packages/webui/src/__mocks__/helpers/database.ts b/packages/webui/src/__mocks__/helpers/database.ts index fbc094fa42..e5d1b62274 100644 --- a/packages/webui/src/__mocks__/helpers/database.ts +++ b/packages/webui/src/__mocks__/helpers/database.ts @@ -72,6 +72,25 @@ export async function setupMockCore(doc?: Partial): Promise { hint?: string item: WrappedOverridableItemNormal itemKey: keyof ReadonlyDeep - opPrefix: string overrideHelper: OverrideOpHelperForItemContents showClearButton?: boolean @@ -32,7 +31,6 @@ export function LabelAndOverrides({ hint, item, itemKey, - opPrefix, overrideHelper, showClearButton, formatDefaultValue, @@ -40,16 +38,16 @@ export function LabelAndOverrides({ const { t } = useTranslation() const clearOverride = useCallback(() => { - overrideHelper().clearItemOverrides(opPrefix, String(itemKey)).commit() - }, [overrideHelper, opPrefix, itemKey]) + overrideHelper().clearItemOverrides(item.id, String(itemKey)).commit() + }, [overrideHelper, item.id, itemKey]) const setValue = useCallback( (newValue: any) => { - overrideHelper().setItemValue(opPrefix, String(itemKey), newValue).commit() + overrideHelper().setItemValue(item.id, String(itemKey), newValue).commit() }, - [overrideHelper, opPrefix, itemKey] + [overrideHelper, item.id, itemKey] ) - const isOverridden = hasOpWithPath(item.overrideOps, opPrefix, String(itemKey)) + const isOverridden = hasOpWithPath(item.overrideOps, item.id, String(itemKey)) let displayValue: JSX.Element | string | null = '""' if (item.defaults) { diff --git a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx index 7a371661c6..b6aa6a0983 100644 --- a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx +++ b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import ClassNames from 'classnames' export function splitValueIntoLines(v: string | undefined): string[] { @@ -85,3 +85,19 @@ export function MultiLineTextInputControl({ /> ) } + +interface ICombinedMultiLineTextInputControlProps + extends Omit { + value: string + handleUpdate: (value: string) => void +} +export function CombinedMultiLineTextInputControl({ + value, + handleUpdate, + ...props +}: Readonly): JSX.Element { + const valueArray = useMemo(() => splitValueIntoLines(value), [value]) + const handleUpdateArray = useCallback((value: string[]) => handleUpdate(joinLines(value)), [handleUpdate]) + + return +} diff --git a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts index 262fe8922c..89c4b34f05 100644 --- a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts +++ b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts @@ -1,8 +1,5 @@ -import { - DBRundownPlaylist, - ForceQuickLoopAutoNext, - QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { PartInstance, wrapPartToTemporaryInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx index bcd00e9afb..2e9779a076 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx @@ -45,7 +45,6 @@ interface FormComponentProps { item: WrappedOverridableItemNormal overrideHelper: OverrideOpHelperForItemContents itemKey: string - opPrefix: string /** Whether a clear button should be showed for fields not marked as "required" */ showClearButton: boolean @@ -68,7 +67,6 @@ function useChildPropsForFormComponent(props: Readonly CoreSystem.findOne(), []) + const coreSystemSettings = useTracker(() => { + const core = CoreSystem.findOne(SYSTEM_ID, { projection: { settingsWithOverrides: 1 } }) + return core && applyAndValidateOverrides(core.settingsWithOverrides).obj + }, []) - const message = coreSystem?.evaluations?.enabled ? coreSystem.evaluations : undefined + const message = coreSystemSettings?.evaluationsMessage?.enabled ? coreSystemSettings.evaluationsMessage : undefined if (!message) return null return ( diff --git a/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx b/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx index ef3e341451..9a0fae510e 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx @@ -12,6 +12,7 @@ import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { useSubscription, useTracker } from '../../../../lib/ReactMeteorData/ReactMeteorData' import { UIBlueprintUpgradeStatuses } from '../../../Collections' import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' +import { UIBlueprintUpgradeStatusShowStyle } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' interface ShowStyleBaseBlueprintConfigurationSettingsProps { showStyleBase: DBShowStyleBase @@ -33,7 +34,7 @@ export function ShowStyleBaseBlueprintConfigurationSettings( UIBlueprintUpgradeStatuses.findOne({ documentId: props.showStyleBase._id, documentType: 'showStyle', - }), + }) as UIBlueprintUpgradeStatusShowStyle | undefined, [props.showStyleBase._id] ) const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') diff --git a/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx b/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx index c0c331ab7d..ad50735d12 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx @@ -270,13 +270,7 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }:
- + {(value, handleUpdate) => ( {(value, handleUpdate) => } @@ -309,7 +302,6 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display Rank')} item={item} itemKey={'_rank'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -325,7 +317,6 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is collapsed by default')} item={item} itemKey={'isDefaultCollapsed'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -334,7 +325,6 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is flattened')} item={item} itemKey={'isFlattened'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } diff --git a/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx b/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx index a7405ffe7a..9b31c7bac9 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -295,13 +295,7 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }:
- + {(value, handleUpdate) => ( {(value, handleUpdate) => ( @@ -341,7 +334,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Source Type')} item={item} itemKey={'type'} - opPrefix={item.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(SourceLayerType)} > @@ -358,7 +350,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is a Live Remote Input')} item={item} itemKey={'isRemoteInput'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -367,7 +358,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is a Guest Input')} item={item} itemKey={'isGuestInput'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -376,7 +366,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is hidden')} item={item} itemKey={'isHidden'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -385,7 +374,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display Rank')} item={item} itemKey={'_rank'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -401,7 +389,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Treat as Main content')} item={item} itemKey={'onPresenterScreen'} - opPrefix={item.id} overrideHelper={overrideHelper} hint="When set, Pieces on this Source Layer will be used to display summaries, thumbnails etc for the Part in GUIs. " > @@ -411,7 +398,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display in a column in List View')} item={item} itemKey={'onListViewColumn'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -420,7 +406,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display AdLibs in a column in List View')} item={item} itemKey={'onListViewAdLibColumn'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -429,7 +414,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Pieces on this layer can be cleared')} item={item} itemKey={'isClearable'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -438,7 +422,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Pieces on this layer are sticky')} item={item} itemKey={'isSticky'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -447,7 +430,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Only Pieces present in rundown are sticky')} item={item} itemKey={'stickyOriginalOnly'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -456,7 +438,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Allow disabling of Pieces')} item={item} itemKey={'allowDisable'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -465,7 +446,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('AdLibs on this layer can be queued')} item={item} itemKey={'isQueueable'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -474,7 +454,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Exclusivity group')} item={item} itemKey={'exclusiveGroup'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( diff --git a/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx b/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx index 9358e54068..78d28da1a0 100644 --- a/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx @@ -15,6 +15,7 @@ import { SelectBlueprint } from './SelectBlueprint' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { UIBlueprintUpgradeStatuses } from '../../../Collections' import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' +import { UIBlueprintUpgradeStatusStudio } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' interface StudioBlueprintConfigurationSettingsProps { studio: DBStudio @@ -31,7 +32,7 @@ export function StudioBlueprintConfigurationSettings( UIBlueprintUpgradeStatuses.findOne({ documentId: props.studio._id, documentType: 'studio', - }), + }) as UIBlueprintUpgradeStatusStudio | undefined, [props.studio._id] ) const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') diff --git a/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx b/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx index b3967bf83e..ae9b79ae30 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx @@ -285,7 +285,6 @@ function SubDeviceEditRow({ label={t('Peripheral Device ID')} item={item} overrideHelper={overrideHelper} - opPrefix={item.id} itemKey={'peripheralDeviceId'} options={peripheralDeviceOptions} > @@ -379,7 +378,6 @@ function SubDeviceEditForm({ peripheralDevice, item, overrideHelper }: Readonly< label={t('Device Type')} item={item} overrideHelper={overrideHelper} - opPrefix={item.id} itemKey={'options.type'} options={subdeviceTypeOptions} > diff --git a/packages/webui/src/client/ui/Settings/Studio/Generic.tsx b/packages/webui/src/client/ui/Settings/Studio/Generic.tsx index 41bc9559c4..bb2302f832 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Generic.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Generic.tsx @@ -1,9 +1,8 @@ import * as React from 'react' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data' +import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { EditAttribute } from '../../../lib/EditAttribute' import { StudioBaselineStatus } from './Baseline' import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -11,9 +10,27 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { Studios } from '../../../collections' import { useHistory } from 'react-router-dom' import { MeteorCall } from '../../../lib/meteorApi' -import { LabelActual } from '../../../lib/Components/LabelAndOverrides' +import { + LabelActual, + LabelAndOverrides, + LabelAndOverridesForCheckbox, + LabelAndOverridesForDropdown, + LabelAndOverridesForInt, +} from '../../../lib/Components/LabelAndOverrides' import { catchError } from '../../../lib/lib' -import { ForceQuickLoopAutoNext } from '@sofie-automation/corelib/src/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +import { + applyAndValidateOverrides, + ObjectWithOverrides, + SomeObjectOverrideOp, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { useOverrideOpHelper, WrappedOverridableItemNormal } from '../util/OverrideOpHelper' +import { IntInputControl } from '../../../lib/Components/IntInput' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { useMemo } from 'react' +import { CheckboxControl } from '../../../lib/Components/Checkbox' +import { TextInputControl } from '../../../lib/Components/TextInput' +import { DropdownInputControl, DropdownInputOption } from '../../../lib/Components/DropdownInput' interface IStudioGenericPropertiesProps { studio: DBStudio @@ -23,327 +40,76 @@ interface IStudioGenericPropertiesProps { showStyleBase: DBShowStyleBase }> } -interface IStudioGenericPropertiesState {} -export const StudioGenericProperties = withTranslation()( - class StudioGenericProperties extends React.Component< - Translated, - IStudioGenericPropertiesState - > { - constructor(props: Translated) { - super(props) - } - renderShowStyleEditButtons() { - const buttons: JSX.Element[] = [] - if (this.props.studio) { - for (const showStyleBaseId of this.props.studio.supportedShowStyleBase) { - const showStyleBase = this.props.availableShowStyleBases.find( - (base) => base.showStyleBase._id === showStyleBaseId - ) - if (showStyleBase) { - buttons.push( - - ) - } - } - } - return buttons - } +export function StudioGenericProperties({ + studio, + availableShowStyleBases, +}: IStudioGenericPropertiesProps): JSX.Element { + const { t } = useTranslation() - render(): JSX.Element { - const { t } = this.props - return ( -
-

{t('Generic Properties')}

- -
- {t('Select Compatible Show Styles')} -
- - {this.renderShowStyleEditButtons()} - -
- {!this.props.studio.supportedShowStyleBase.length ? ( -
- {t('Show style not set')} -
- ) : null} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ const showStyleEditButtons: JSX.Element[] = [] + for (const showStyleBaseId of studio.supportedShowStyleBase) { + const showStyleBase = availableShowStyleBases.find((base) => base.showStyleBase._id === showStyleBaseId) + if (showStyleBase) { + showStyleEditButtons.push( + ) } } -) + + return ( +
+

{t('Generic Properties')}

+ +
+ {t('Select Compatible Show Styles')} +
+ + {showStyleEditButtons} + +
+ {!studio.supportedShowStyleBase.length ? ( +
+ {t('Show style not set')} +
+ ) : null} +
+ + + + +
+ ) +} const NewShowStyleButton = React.memo(function NewShowStyleButton() { const history = useHistory() @@ -378,3 +144,303 @@ const RedirectToShowStyleButton = React.memo(function RedirectToShowStyleButton( ) }) + +function StudioSettings({ studio }: { studio: DBStudio }): JSX.Element { + const { t } = useTranslation() + + const saveOverrides = React.useCallback( + (newOps: SomeObjectOverrideOp[]) => { + Studios.update(studio._id, { + $set: { + 'settingsWithOverrides.overrides': newOps.map((op) => ({ + ...op, + path: op.path.startsWith('0.') ? op.path.slice(2) : op.path, + })), + }, + }) + }, + [studio._id] + ) + + const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const prefixedOps = studio.settingsWithOverrides.overrides.map((op) => ({ + ...op, + // TODO: can we avoid doing this hack? + path: `0.${op.path}`, + })) + + const computedValue = applyAndValidateOverrides(studio.settingsWithOverrides).obj + + const wrappedItem = literal>({ + type: 'normal', + id: '0', + computed: computedValue, + defaults: studio.settingsWithOverrides.defaults, + overrideOps: prefixedOps, + }) + + const wrappedConfigObject: ObjectWithOverrides = { + defaults: studio.settingsWithOverrides.defaults, + overrides: prefixedOps, + } + + return [wrappedItem, wrappedConfigObject] + }, [studio.settingsWithOverrides]) + + const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + + const autoNextOptions: DropdownInputOption[] = useMemo( + () => [ + { + name: t('Disabled'), + value: ForceQuickLoopAutoNext.DISABLED, + i: 0, + }, + { + name: t('Enabled, but skipping parts with undefined or 0 duration'), + value: ForceQuickLoopAutoNext.ENABLED_WHEN_VALID_DURATION, + i: 1, + }, + { + name: t('Enabled on all Parts, applying QuickLoop Fallback Part Duration if needed'), + value: ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION, + i: 2, + }, + ], + [t] + ) + + return ( + <> + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate, options) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx b/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx index f47a0bc4fc..5807d916e3 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx @@ -432,7 +432,6 @@ function StudioMappingsEntry({ hint={t('Human-readable name of the layer')} item={item} itemKey={'layerName'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -450,7 +449,6 @@ function StudioMappingsEntry({ hint={t('The type of device to use for the output')} item={item} itemKey={'device'} - opPrefix={item.id} overrideHelper={overrideHelper} options={deviceTypeOptions} > @@ -469,7 +467,6 @@ function StudioMappingsEntry({ hint={t('ID of the device (corresponds to the device ID in the peripheralDevice settings)')} item={item} itemKey={'deviceId'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -486,7 +483,6 @@ function StudioMappingsEntry({ label={t('Lookahead Mode')} item={item} itemKey={'lookahead'} - opPrefix={item.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(LookaheadMode)} > @@ -504,7 +500,6 @@ function StudioMappingsEntry({ label={t('Lookahead Target Objects (Undefined = 1)')} item={item} itemKey={'lookaheadDepth'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -523,7 +518,6 @@ function StudioMappingsEntry({ })} item={item} itemKey={'lookaheadMaxSearchDistance'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -543,7 +537,6 @@ function StudioMappingsEntry({ hint={t('The type of mapping to use')} item={item} itemKey={'options.mappingType'} - opPrefix={item.id} overrideHelper={overrideHelper} options={mappingTypeOptions} > diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx index eee92146cd..780a8afb12 100644 --- a/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx @@ -133,7 +133,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.label`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -150,7 +149,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.type`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(Accessor.AccessType)} > @@ -173,7 +171,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.folderPath`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -191,7 +188,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.resourceId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -212,7 +208,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.baseUrl`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -230,7 +225,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.isImmutable`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -249,7 +243,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.useGETinsteadOfHEAD`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -269,7 +262,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.networkId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -290,7 +282,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.baseUrl`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -310,7 +301,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.networkId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -331,7 +321,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.folderPath`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -349,7 +338,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.userName`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -367,7 +355,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.password`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -385,7 +372,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.networkId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -406,7 +392,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.quantelGatewayUrl`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -424,7 +409,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.ISAUrls`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -442,7 +426,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.zoneId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -462,7 +445,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.serverId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -480,7 +462,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.transformerURL`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -498,7 +479,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.fileflowURL`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -516,7 +496,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.fileflowProfile`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -536,7 +515,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.allowRead`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -546,7 +524,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.allowWrite`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx index d92e3e31c8..ac915bfbab 100644 --- a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx @@ -285,7 +285,6 @@ function PackageContainerRow({ item={packageContainer} //@ts-expect-error can't be 2 levels deep itemKey={'container.label'} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -302,7 +301,6 @@ function PackageContainerRow({ hint={t('Select which playout devices are using this package container')} item={packageContainer} itemKey={'deviceIds'} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} options={availablePlayoutDevicesOptions} > diff --git a/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx b/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx index 7b3a167749..ec442fd0cc 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx @@ -223,7 +223,6 @@ function ExclusivityGroupRow({ label={t('Exclusivity Group Name')} item={exclusivityGroup} itemKey={'name'} - opPrefix={exclusivityGroup.id} overrideHelper={exclusivityOverrideHelper} > {(value, handleUpdate) => ( diff --git a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx index f6dc204d50..83ec047d7a 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx @@ -118,7 +118,6 @@ function AbPlayerRow({ label={t('Pool name')} item={player} itemKey={'poolName'} - opPrefix={player.id} overrideHelper={tableOverrideHelper} > {(value, handleUpdate) => ( @@ -134,7 +133,6 @@ function AbPlayerRow({ label={t('Pool PlayerId')} item={player} itemKey={'playerId'} - opPrefix={player.id} overrideHelper={tableOverrideHelper} > {(value, handleUpdate) => ( diff --git a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx index cf555b4611..fe82a55647 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx @@ -306,7 +306,6 @@ function RouteSetRow({ hint={t('he default state of this Route Set')} item={routeSet} itemKey={'defaultActive'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(DEFAULT_ACTIVE_OPTIONS)} > @@ -323,7 +322,6 @@ function RouteSetRow({ label={t('Active')} item={routeSet} itemKey={'active'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -332,7 +330,6 @@ function RouteSetRow({ label={t('Route Set Name')} item={routeSet} itemKey={'name'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -350,7 +347,6 @@ function RouteSetRow({ hint={t('If set, only one Route Set will be active per exclusivity group')} item={routeSet} itemKey={'exclusivityGroup'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} options={exclusivityGroupOptions} > @@ -369,7 +365,6 @@ function RouteSetRow({ hint={t('The way this Route Set should behave towards the user')} item={routeSet} itemKey={'behavior'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(StudioRouteBehavior)} > @@ -601,7 +596,6 @@ function RenderRoutesRow({ label={t('Original Layer')} item={route} itemKey={'mappedLayer'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={getDropdownInputOptions(Object.keys(studioMappings))} > @@ -619,7 +613,6 @@ function RenderRoutesRow({ label={t('New Layer')} item={route} itemKey={'outputMappedLayer'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} > {(value, handleUpdate) => ( @@ -636,7 +629,6 @@ function RenderRoutesRow({ label={t('Route Type')} item={route} itemKey={'routeType'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={getDropdownInputOptions(StudioRouteType)} > @@ -660,7 +652,6 @@ function RenderRoutesRow({ label={t('Device Type')} item={route} itemKey={'deviceType'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={getDropdownInputOptions(TSR.DeviceType)} > @@ -689,7 +680,6 @@ function RenderRoutesRow({ label={t('Mapping Type')} item={route} itemKey={'remapping.options.mappingType'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={mappingTypeOptions} > @@ -710,7 +700,6 @@ function RenderRoutesRow({ label={t('Device ID')} item={route} itemKey={'remapping.deviceId'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} showClearButton={true} > diff --git a/packages/webui/src/client/ui/Settings/SystemManagement.tsx b/packages/webui/src/client/ui/Settings/SystemManagement.tsx index 742e04e5b2..b15eef1622 100644 --- a/packages/webui/src/client/ui/Settings/SystemManagement.tsx +++ b/packages/webui/src/client/ui/Settings/SystemManagement.tsx @@ -9,14 +9,30 @@ import { languageAnd } from '../../lib/language' import { TriggeredActionsEditor } from './components/triggeredActions/TriggeredActionsEditor' import { TFunction, useTranslation } from 'react-i18next' import { Meteor } from 'meteor/meteor' -import { LogLevel } from '../../lib/tempLib' +import { literal, LogLevel } from '../../lib/tempLib' import { CoreSystem } from '../../collections' import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system' -import { LabelActual } from '../../lib/Components/LabelAndOverrides' +import { + LabelActual, + LabelAndOverrides, + LabelAndOverridesForCheckbox, + LabelAndOverridesForMultiLineText, +} from '../../lib/Components/LabelAndOverrides' import { catchError } from '../../lib/lib' +import { SystemManagementBlueprint } from './SystemManagement/Blueprint' +import { + applyAndValidateOverrides, + ObjectWithOverrides, + SomeObjectOverrideOp, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { ICoreSystemSettings } from '@sofie-automation/blueprints-integration' +import { WrappedOverridableItemNormal, useOverrideOpHelper } from './util/OverrideOpHelper' +import { CheckboxControl } from '../../lib/Components/Checkbox' +import { CombinedMultiLineTextInputControl, MultiLineTextInputControl } from '../../lib/Components/MultiLineTextInput' +import { TextInputControl } from '../../lib/Components/TextInput' interface WithCoreSystemProps { - coreSystem: ICoreSystem | undefined + coreSystem: ICoreSystem } export default function SystemManagement(): JSX.Element | null { @@ -30,6 +46,8 @@ export default function SystemManagement(): JSX.Element | null {
+ + @@ -156,25 +174,29 @@ function SystemManagementNotificationMessage({ coreSystem }: Readonly) { const { t } = useTranslation() + const { wrappedItem, overrideHelper } = useCoreSystemSettingsWithOverrides(coreSystem) + return ( <>

{t('Support Panel')}

- + )} +
) @@ -183,50 +205,56 @@ function SystemManagementSupportPanel({ coreSystem }: Readonly) { const { t } = useTranslation() + const { wrappedItem, overrideHelper } = useCoreSystemSettingsWithOverrides(coreSystem) + return ( <>

{t('Evaluations')}

- - - + )} +
) @@ -304,55 +332,49 @@ function SystemManagementMonitoring({ coreSystem }: Readonly) { const { t } = useTranslation() + const { wrappedItem, overrideHelper } = useCoreSystemSettingsWithOverrides(coreSystem) + return ( <>

{t('Cron jobs')}

- - - + )} +
) @@ -571,3 +593,51 @@ function SystemManagementHeapSnapshot() { ) } + +function useCoreSystemSettingsWithOverrides(coreSystem: ICoreSystem) { + const saveOverrides = useCallback( + (newOps: SomeObjectOverrideOp[]) => { + CoreSystem.update(coreSystem._id, { + $set: { + 'settingsWithOverrides.overrides': newOps.map((op) => ({ + ...op, + path: op.path.startsWith('0.') ? op.path.slice(2) : op.path, + })), + }, + }) + }, + [coreSystem._id] + ) + + const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const prefixedOps = coreSystem.settingsWithOverrides.overrides.map((op) => ({ + ...op, + // TODO: can we avoid doing this hack? + path: `0.${op.path}`, + })) + + const computedValue = applyAndValidateOverrides(coreSystem.settingsWithOverrides).obj + + const wrappedItem = literal>({ + type: 'normal', + id: '0', + computed: computedValue, + defaults: coreSystem.settingsWithOverrides.defaults, + overrideOps: prefixedOps, + }) + + const wrappedConfigObject: ObjectWithOverrides = { + defaults: coreSystem.settingsWithOverrides.defaults, + overrides: prefixedOps, + } + + return [wrappedItem, wrappedConfigObject] + }, [coreSystem.settingsWithOverrides]) + + const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + + return { + wrappedItem, + overrideHelper, + } +} diff --git a/packages/webui/src/client/ui/Settings/SystemManagement/Blueprint.tsx b/packages/webui/src/client/ui/Settings/SystemManagement/Blueprint.tsx new file mode 100644 index 0000000000..ce87ee092e --- /dev/null +++ b/packages/webui/src/client/ui/Settings/SystemManagement/Blueprint.tsx @@ -0,0 +1,99 @@ +import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import { UIBlueprintUpgradeStatusCoreSystem } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { useTranslation } from 'react-i18next' +import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { UIBlueprintUpgradeStatuses } from '../../Collections' +import { getUpgradeStatusMessage, SystemUpgradeStatusButtons } from '../Upgrades/Components' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { Blueprints, CoreSystem } from '../../../collections' +import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { useMemo } from 'react' +import { LabelActual } from '../../../lib/Components/LabelAndOverrides' +import { EditAttribute } from '../../../lib/EditAttribute' +import { RedirectToBlueprintButton } from '../../../lib/SettingsNavigation' + +interface SystemManagementBlueprintProps { + coreSystem: ICoreSystem | undefined +} +export function SystemManagementBlueprint({ coreSystem }: Readonly): JSX.Element { + const { t } = useTranslation() + + const isStatusReady = useSubscription(MeteorPubSub.uiBlueprintUpgradeStatuses) + const status = useTracker( + () => + coreSystem && + (UIBlueprintUpgradeStatuses.findOne({ + documentId: coreSystem._id, + documentType: 'coreSystem', + }) as UIBlueprintUpgradeStatusCoreSystem | undefined), + [coreSystem?._id] + ) + const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') + + return ( +
+
+ + +

+ {t('Upgrade Status')}: {statusMessage} + {status && } +

+
+
+ ) +} + +interface SelectBlueprintProps { + coreSystem: ICoreSystem | undefined +} + +function SelectBlueprint({ coreSystem }: Readonly): JSX.Element { + const { t } = useTranslation() + + const allSystemBlueprints = useTracker(() => { + return Blueprints.find({ + blueprintType: BlueprintManifestType.SYSTEM, + }).fetch() + }, []) + const blueprintOptions: { name: string; value: BlueprintId | null }[] = useMemo(() => { + if (allSystemBlueprints) { + return allSystemBlueprints.map((blueprint) => { + return { + name: blueprint.name ? `${blueprint.name} (${blueprint._id})` : unprotectString(blueprint._id), + value: blueprint._id, + } + }) + } else { + return [] + } + }, [allSystemBlueprints]) + + return ( +
+ +
+ ) +} diff --git a/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx b/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx index 19e7fc1778..1034a6a91a 100644 --- a/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx +++ b/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx @@ -10,6 +10,7 @@ import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { NotificationCenter, NoticeLevel, Notification } from '../../../lib/notifications/notifications' import { UIBlueprintUpgradeStatusBase, + UIBlueprintUpgradeStatusCoreSystem, UIBlueprintUpgradeStatusShowStyle, UIBlueprintUpgradeStatusStudio, } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' @@ -288,3 +289,76 @@ export function UpgradeStatusButtons({ upgradeResult }: Readonly ) } + +interface SystemUpgradeStatusButtonsProps { + upgradeResult: UIBlueprintUpgradeStatusCoreSystem +} +export function SystemUpgradeStatusButtons({ upgradeResult }: Readonly): JSX.Element { + const { t } = useTranslation() + + const applyConfig = useCallback( + async () => MeteorCall.migration.runUpgradeForCoreSystem(upgradeResult.documentId), + [upgradeResult.documentId, upgradeResult.documentType] + ) + + const clickApply = useCallback(() => { + applyConfig() + .then(() => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + .catch((e) => { + catchError('Upgrade applyConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, [upgradeResult, applyConfig]) + + const clickShowChanges = useCallback(() => { + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: ( +
+ {upgradeResult.changes.length === 0 &&

{t('No changes')}

} + {upgradeResult.changes.map((msg, i) => ( +

{translateMessage(msg, i18nTranslator)}

+ ))} +
+ ), + acceptOnly: true, + yes: t('Dismiss'), + onAccept: () => { + // Do nothing + }, + }) + }, [upgradeResult]) + + return ( +
+ + +
+ ) +} diff --git a/packages/webui/src/client/ui/Settings/Upgrades/View.tsx b/packages/webui/src/client/ui/Settings/Upgrades/View.tsx index 2e6f2bbfd7..7302d2acd0 100644 --- a/packages/webui/src/client/ui/Settings/Upgrades/View.tsx +++ b/packages/webui/src/client/ui/Settings/Upgrades/View.tsx @@ -4,11 +4,8 @@ import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { UIBlueprintUpgradeStatuses } from '../../Collections' -import { - UIBlueprintUpgradeStatusShowStyle, - UIBlueprintUpgradeStatusStudio, -} from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' -import { getUpgradeStatusMessage, UpgradeStatusButtons } from './Components' +import { UIBlueprintUpgradeStatus } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { getUpgradeStatusMessage, SystemUpgradeStatusButtons, UpgradeStatusButtons } from './Components' export function UpgradesView(): JSX.Element { const { t } = useTranslation() @@ -39,6 +36,17 @@ export function UpgradesView(): JSX.Element { )} + {statuses?.map( + (document) => + document.documentType === 'coreSystem' && ( + + ) + )} + {statuses?.map( (document) => document.documentType === 'studio' && ( @@ -69,7 +77,7 @@ export function UpgradesView(): JSX.Element { interface ShowUpgradesRowProps { resourceName: string - upgradeResult: UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle + upgradeResult: UIBlueprintUpgradeStatus } function ShowUpgradesRow({ resourceName, upgradeResult }: Readonly) { const { t } = useTranslation() @@ -83,7 +91,11 @@ function ShowUpgradesRow({ resourceName, upgradeResult }: Readonly{getUpgradeStatusMessage(t, upgradeResult)} - + {upgradeResult.documentType === 'coreSystem' ? ( + + ) : ( + + )} ) diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index 8a623bc773..d313b89a5c 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -437,7 +437,7 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction {showStyleBaseId !== null ? ( <>
- {(systemTriggeredActionIds?.length ?? 0) > 0 && !parsedTriggerFilter ? ( + {!parsedTriggerFilter ? (

setSystemWideCollapsed(!systemWideCollapsed)} @@ -470,13 +470,19 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction /> )) : null} + + {!systemWideCollapsed && !parsedTriggerFilter && systemTriggeredActionIds?.length === 0 && ( +

{t('No Action Triggers set up.')}

+ )}

) : null}
- + + + { - const core = CoreSystem.findOne() + const core = CoreSystem.findOne(SYSTEM_ID, { projection: { settingsWithOverrides: 1 } }) + const coreSettings = core && applyAndValidateOverrides(core.settingsWithOverrides).obj return { - supportMessage: core?.support?.message ?? '', + supportMessage: coreSettings?.support?.message ?? '', } }, [],