diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index bab46f41aa..e254257340 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -114,7 +114,7 @@ export function defaultStudio(_id: StudioId): DBStudio { _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), previewContainerIds: [], thumbnailContainerIds: [], peripheralDeviceSettings: { diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index b8788de6e3..a9c43b9bf0 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -279,7 +279,7 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P routeSetsWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), previewContainerIds: [], thumbnailContainerIds: [], peripheralDeviceSettings: { diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 4a646967d2..70e7699f80 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -52,7 +52,7 @@ export async function insertStudioInner(organizationId: OrganizationId | null, n _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), thumbnailContainerIds: [], previewContainerIds: [], peripheralDeviceSettings: { diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index 15aafc3451..80247e212b 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -447,7 +447,7 @@ export const addSteps = addMigrationSteps('0.1.0', [ _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), thumbnailContainerIds: [], previewContainerIds: [], peripheralDeviceSettings: { diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 2d3cc1eff7..9ca7a7742c 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -2,7 +2,11 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' import { Studios } from '../collections' import { convertObjectIntoOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { StudioRouteSet, StudioRouteSetExclusivityGroup } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { + StudioRouteSet, + StudioRouteSetExclusivityGroup, + StudioPackageContainer, +} from '@sofie-automation/corelib/dist/dataModel/Studio' /* * ************************************************************************************** @@ -99,4 +103,41 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ } }, }, + { + id: `convert packageContainers to ObjectWithOverrides`, + canBeRunAutomatically: true, + validate: async () => { + const studios = await Studios.findFetchAsync({ packageContainers: { $exists: true } }) + + for (const studio of studios) { + //@ts-expect-error packageContainers is not typed as ObjectWithOverrides + if (studio.packageContainers) { + return 'packageContainers must be converted to an ObjectWithOverrides' + } + } + + return false + }, + migrate: async () => { + const studios = await Studios.findFetchAsync({ packageContainers: { $exists: true } }) + + for (const studio of studios) { + //@ts-expect-error packageContainers is not typed as ObjectWithOverrides + if (!studio.packageContainers) continue + //@ts-expect-error packageContainers is not typed as ObjectWithOverrides + const oldPackageContainers = studio.packageContainers as any as Record + + const newPackageContainers = convertObjectIntoOverrides(oldPackageContainers) + + await Studios.updateAsync(studio._id, { + $set: { + packageContainersWithOverrides: newPackageContainers, + }, + $unset: { + packageContainers: 1, + }, + }) + } + }, + }, ]) diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 3278b7a9ee..f4ec9ac82c 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -132,7 +132,7 @@ describe('Migrations', () => { _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), previewContainerIds: [], thumbnailContainerIds: [], peripheralDeviceSettings: { @@ -170,7 +170,7 @@ describe('Migrations', () => { _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), previewContainerIds: [], thumbnailContainerIds: [], peripheralDeviceSettings: { @@ -208,7 +208,7 @@ describe('Migrations', () => { _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), previewContainerIds: [], thumbnailContainerIds: [], peripheralDeviceSettings: { diff --git a/meteor/server/publications/packageManager/expectedPackages/generate.ts b/meteor/server/publications/packageManager/expectedPackages/generate.ts index 4850ceb301..b84c6482a1 100644 --- a/meteor/server/publications/packageManager/expectedPackages/generate.ts +++ b/meteor/server/publications/packageManager/expectedPackages/generate.ts @@ -17,6 +17,7 @@ import { CustomPublishCollection } from '../../../lib/customPublication' import { logger } from '../../../logging' import { ExpectedPackagesContentCache } from './contentCache' import type { StudioFields } from './publication' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' /** * Regenerate the output for the provided ExpectedPackage `regenerateIds`, updating the data in `collection` as needed @@ -37,6 +38,7 @@ export async function updateCollectionForExpectedPackageIds( ): Promise { const updatedDocIds = new Set() const missingExpectedPackageIds = new Set() + const packageContainers = applyAndValidateOverrides(studio.packageContainersWithOverrides).obj for (const packageId of regenerateIds) { const packageDoc = contentCache.ExpectedPackages.findOne(packageId) @@ -66,7 +68,8 @@ export async function updateCollectionForExpectedPackageIds( }, deviceId, null, - Priorities.OTHER // low priority + Priorities.OTHER, // low priority + packageContainers ) updatedDocIds.add(routedPackage._id) @@ -105,6 +108,7 @@ export async function updateCollectionForPieceInstanceIds( ): Promise { const updatedDocIds = new Set() const missingPieceInstanceIds = new Set() + const packageContainers = applyAndValidateOverrides(studio.packageContainersWithOverrides).obj for (const pieceInstanceId of regenerateIds) { const pieceInstanceDoc = contentCache.PieceInstances.findOne(pieceInstanceId) @@ -140,7 +144,8 @@ export async function updateCollectionForPieceInstanceIds( }, deviceId, pieceInstanceId, - Priorities.OTHER // low priority + Priorities.OTHER, // low priority + packageContainers ) updatedDocIds.add(routedPackage._id) @@ -172,17 +177,21 @@ enum Priorities { } function generateExpectedPackageForDevice( - studio: Pick, + studio: Pick< + StudioLight, + '_id' | 'packageContainersWithOverrides' | 'previewContainerIds' | 'thumbnailContainerIds' + >, expectedPackage: PackageManagerExpectedPackageBase, deviceId: PeripheralDeviceId, pieceInstanceId: PieceInstanceId | null, - priority: Priorities + priority: Priorities, + packageContainers: Record ): PackageManagerExpectedPackage { // Lookup Package sources: const combinedSources: PackageContainerOnPackage[] = [] for (const packageSource of expectedPackage.sources) { - const lookedUpSource = studio.packageContainers[packageSource.containerId] + const lookedUpSource = packageContainers[packageSource.containerId] if (lookedUpSource) { combinedSources.push(calculateCombinedSource(packageSource, lookedUpSource)) } else { @@ -199,7 +208,7 @@ function generateExpectedPackageForDevice( } // Lookup Package targets: - const combinedTargets = calculateCombinedTargets(studio, expectedPackage, deviceId) + const combinedTargets = calculateCombinedTargets(expectedPackage, deviceId, packageContainers) if (!combinedSources.length && expectedPackage.sources.length !== 0) { logger.warn(`Pub.expectedPackagesForDevice: No sources found for "${expectedPackage._id}"`) @@ -253,14 +262,14 @@ function calculateCombinedSource( return combinedSource } function calculateCombinedTargets( - studio: Pick, expectedPackage: PackageManagerExpectedPackageBase, - deviceId: PeripheralDeviceId + deviceId: PeripheralDeviceId, + packageContainers: Record ): PackageContainerOnPackage[] { const mappingDeviceId = unprotectString(deviceId) let packageContainerId: string | undefined - for (const [containerId, packageContainer] of Object.entries(studio.packageContainers)) { + for (const [containerId, packageContainer] of Object.entries(packageContainers)) { if (packageContainer.deviceIds.includes(mappingDeviceId)) { // TODO: how to handle if a device has multiple containers? packageContainerId = containerId @@ -270,7 +279,7 @@ function calculateCombinedTargets( const combinedTargets: PackageContainerOnPackage[] = [] if (packageContainerId) { - const lookedUpTarget = studio.packageContainers[packageContainerId] + const lookedUpTarget = packageContainers[packageContainerId] if (lookedUpTarget) { // Todo: should the be any combination of properties here? combinedTargets.push({ diff --git a/meteor/server/publications/packageManager/expectedPackages/publication.ts b/meteor/server/publications/packageManager/expectedPackages/publication.ts index 969786b2b4..02455803a9 100644 --- a/meteor/server/publications/packageManager/expectedPackages/publication.ts +++ b/meteor/server/publications/packageManager/expectedPackages/publication.ts @@ -56,14 +56,14 @@ export type StudioFields = | '_id' | 'routeSetsWithOverrides' | 'mappingsWithOverrides' - | 'packageContainers' + | 'packageContainersWithOverrides' | 'previewContainerIds' | 'thumbnailContainerIds' const studioFieldSpecifier = literal>>({ _id: 1, routeSetsWithOverrides: 1, mappingsWithOverrides: 1, - packageContainers: 1, + packageContainersWithOverrides: 1, previewContainerIds: 1, thumbnailContainerIds: 1, }) diff --git a/meteor/server/publications/packageManager/packageContainers.ts b/meteor/server/publications/packageManager/packageContainers.ts index a479f8d66a..8c43c0a611 100644 --- a/meteor/server/publications/packageManager/packageContainers.ts +++ b/meteor/server/publications/packageManager/packageContainers.ts @@ -15,11 +15,12 @@ import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -type StudioFields = '_id' | 'packageContainers' +type StudioFields = '_id' | 'packageContainersWithOverrides' const studioFieldSpecifier = literal>>({ _id: 1, - packageContainers: 1, + packageContainersWithOverrides: 1, }) interface PackageManagerPackageContainersArgs { @@ -68,8 +69,9 @@ async function manipulateExpectedPackagesPublicationData( const packageContainers: { [containerId: string]: PackageContainer } = {} if (studio) { + const studioPackageContainers = applyAndValidateOverrides(studio.packageContainersWithOverrides).obj for (const [containerId, studioPackageContainer] of Object.entries( - studio.packageContainers + studioPackageContainers )) { packageContainers[containerId] = studioPackageContainer.container } diff --git a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts index 9d4138eadb..f0d34355f5 100644 --- a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts +++ b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts @@ -5,6 +5,7 @@ import { getMediaObjectMediaId, PieceContentStreamInfo, checkPieceContentStatusAndDependencies, + PieceContentStatusStudio, } from '../checkPieceContentStatus' import { PackageInfo, @@ -31,12 +32,10 @@ import { MediaStream, MediaStreamType, } from '@sofie-automation/shared-lib/dist/core/model/MediaObjects' -import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { defaultStudio } from '../../../../__mocks__/defaultCollectionObjects' import { testInFiber } from '../../../../__mocks__/helpers/jest' import { MediaObjects } from '../../../collections' import { PieceDependencies } from '../common' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' const mockMediaObjectsCollection = MongoMock.getInnerMockCollection(MediaObjects) @@ -174,17 +173,14 @@ describe('lib/mediaObjects', () => { } const mockDefaultStudio = defaultStudio(protectString('studio0')) - const mockStudio: Complete< - Pick & - Pick - > = { + const mockStudio: Complete = { _id: mockDefaultStudio._id, settings: mockStudioSettings, - packageContainers: mockDefaultStudio.packageContainers, previewContainerIds: ['previews0'], thumbnailContainerIds: ['thumbnails0'], routeSets: applyAndValidateOverrides(mockDefaultStudio.routeSetsWithOverrides).obj, mappings: applyAndValidateOverrides(mockDefaultStudio.mappingsWithOverrides).obj, + packageContainers: applyAndValidateOverrides(mockDefaultStudio.packageContainersWithOverrides).obj, } mockMediaObjectsCollection.insert( diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index e90b34a2d8..987d865031 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -173,11 +173,15 @@ export type PieceContentStatusPiece = Pick { + extends Pick { /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt /** Route sets with overrides */ routeSets: 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 async function checkPieceContentStatusAndDependencies( @@ -557,7 +561,7 @@ async function checkPieceContentExpectedPackageStatus( const sideEffect = getSideEffect(expectedPackage, studio) thumbnailUrl = await getAssetUrlFromPackageContainerStatus( - studio, + studio.packageContainers, getPackageContainerPackageStatus, expectedPackageId, sideEffect.thumbnailContainerId, @@ -569,7 +573,7 @@ async function checkPieceContentExpectedPackageStatus( const sideEffect = getSideEffect(expectedPackage, studio) previewUrl = await getAssetUrlFromPackageContainerStatus( - studio, + studio.packageContainers, getPackageContainerPackageStatus, expectedPackageId, sideEffect.previewContainerId, @@ -716,7 +720,7 @@ async function checkPieceContentExpectedPackageStatus( } async function getAssetUrlFromPackageContainerStatus( - studio: PieceContentStatusStudio, + packageContainers: Record, getPackageContainerPackageStatus: ( packageContainerId: string, expectedPackageId: ExpectedPackageId @@ -727,7 +731,7 @@ async function getAssetUrlFromPackageContainerStatus( ): Promise { if (!assetContainerId || !packageAssetPath) return - const assetPackageContainer = studio.packageContainers[assetContainerId] + const assetPackageContainer = packageContainers[assetContainerId] if (!assetPackageContainer) return const previewPackageOnPackageContainer = await getPackageContainerPackageStatus(assetContainerId, expectedPackageId) diff --git a/meteor/server/publications/pieceContentStatusUI/common.ts b/meteor/server/publications/pieceContentStatusUI/common.ts index f271150973..591f1eb16e 100644 --- a/meteor/server/publications/pieceContentStatusUI/common.ts +++ b/meteor/server/publications/pieceContentStatusUI/common.ts @@ -14,7 +14,7 @@ import { PieceContentStatusStudio } from './checkPieceContentStatus' export type StudioFields = | '_id' | 'settings' - | 'packageContainers' + | 'packageContainersWithOverrides' | 'previewContainerIds' | 'thumbnailContainerIds' | 'mappingsWithOverrides' @@ -22,7 +22,7 @@ export type StudioFields = export const studioFieldSpecifier = literal>>({ _id: 1, settings: 1, - packageContainers: 1, + packageContainersWithOverrides: 1, previewContainerIds: 1, thumbnailContainerIds: 1, mappingsWithOverrides: 1, @@ -113,10 +113,10 @@ export async function fetchStudio(studioId: StudioId): Promise extends BlueprintManifestBase { @@ -116,6 +117,8 @@ export interface BlueprintResultApplyStudioConfig { routeSets?: Record /** Route Set Exclusivity Groups */ routeSetExclusivityGroups?: Record + /** Package Containers */ + packageContainers?: Record } export interface IStudioConfigPreset { diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index 1315895466..c3f8fd2c53 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -1,4 +1,4 @@ -import { IBlueprintConfig, PackageContainer, TSR } from '@sofie-automation/blueprints-integration' +import { IBlueprintConfig, TSR } from '@sofie-automation/blueprints-integration' import { ObjectWithOverrides } from '../settings/objectWithOverrides' import { StudioId, OrganizationId, BlueprintId, ShowStyleBaseId, MappingsHash, PeripheralDeviceId } from './Ids' import { BlueprintHash, LastBlueprintConfig } from './Blueprint' @@ -13,6 +13,7 @@ import { StudioRouteSetExclusivityGroup, StudioRouteType, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' +import { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' export { MappingsExt, MappingExt, MappingsHash } @@ -26,6 +27,7 @@ export { ResultingMappingRoutes, StudioRouteSet, StudioRouteType, + StudioPackageContainer, } export interface IStudioSettings { @@ -128,7 +130,7 @@ export interface DBStudio { /** Contains settings for which Package Containers are present in the studio. * (These are used by the Package Manager and the Expected Packages) */ - packageContainers: Record + packageContainersWithOverrides: ObjectWithOverrides> /** Which package containers is used for media previews in GUI */ previewContainerIds: string[] @@ -182,9 +184,3 @@ export interface StudioPlayoutDevice { options: TSR.DeviceOptionsAny } - -export interface StudioPackageContainer { - /** List of which peripheraldevices uses this packageContainer */ - deviceIds: string[] - container: PackageContainer -} diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 8f1ce6389f..171a929dba 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -110,7 +110,7 @@ export function defaultStudio(_id: StudioId): DBStudio { }, routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), previewContainerIds: [], thumbnailContainerIds: [], peripheralDeviceSettings: { diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index 9daaaf0727..c685420960 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -9,6 +9,7 @@ import { MappingsExt, StudioIngestDevice, StudioInputDevice, + StudioPackageContainer, StudioPlayoutDevice, StudioRouteSet, StudioRouteSetExclusivityGroup, @@ -97,6 +98,16 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data ]) ) + const packageContainers = Object.fromEntries( + Object.entries(result.packageContainers ?? {}).map((dev) => [ + dev[0], + literal>({ + deviceIds: (dev[1] as StudioPackageContainer).deviceIds, + container: (dev[1] as StudioPackageContainer).container, + }), + ]) + ) + await context.directCollections.Studios.update(context.studioId, { $set: { 'mappingsWithOverrides.defaults': translateMappings(result.mappings), @@ -105,6 +116,7 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data 'peripheralDeviceSettings.inputDevices.defaults': inputDevices, 'routeSetsWithOverrides.defaults': routeSets, 'routeSetExclusivityGroupsWithOverrides.defaults': routeSetExclusivityGroups, + 'packageContainersWithOverrides.defaults': packageContainers, lastBlueprintConfig: { blueprintHash: blueprint.blueprintDoc.blueprintHash, blueprintId: blueprint.blueprintId, diff --git a/packages/shared-lib/src/core/model/PackageContainer.ts b/packages/shared-lib/src/core/model/PackageContainer.ts new file mode 100644 index 0000000000..f2eeeab894 --- /dev/null +++ b/packages/shared-lib/src/core/model/PackageContainer.ts @@ -0,0 +1,7 @@ +import { PackageContainer } from '../../package-manager/package' + +export interface StudioPackageContainer { + /** List of which peripheraldevices uses this packageContainer */ + deviceIds: string[] + container: PackageContainer +} diff --git a/packages/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index 4a699eb99e..99dfea69b7 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -109,7 +109,7 @@ export function defaultStudio(_id: StudioId): DBStudio { _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainers: {}, + packageContainersWithOverrides: wrapDefaultObject({}), previewContainerIds: [], thumbnailContainerIds: [], peripheralDeviceSettings: { diff --git a/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx b/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx index ef291cafc8..be6c4d39f2 100644 --- a/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx +++ b/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx @@ -199,3 +199,44 @@ export function LabelAndOverridesForBase64Image( return {...props} formatDefaultValue={formatter} /> } + +export function LabelAndOverridesForMultiSelect( + props: Omit, 'formatDefaultValue' | 'children'> & { + options: DropdownInputOption[] + children: ( + value: TValue[], + setValue: (value: TValue[]) => void, + options: DropdownInputOption[] + ) => React.ReactNode + } +): JSX.Element { + const formatMultiLine = useCallback( + (value: any) => { + const matchedOption = findOptionByValue(props.options, value) + if (matchedOption) { + return `"${matchedOption.name}"` + } else { + return `Value: "${value}"` + } + }, + [props.options] + ) + const formatter = useCallback( + (defaultValue: any) => { + if (defaultValue === undefined || defaultValue.length === 0) return '""' + + if (Array.isArray(defaultValue)) { + return defaultValue.map(formatMultiLine).join('/n') + } else { + return formatMultiLine(defaultValue) + } + }, + [formatMultiLine] + ) + + return ( + {...props} formatDefaultValue={formatter}> + {(value, setValue) => props.children(value, setValue, props.options)} + + ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager.tsx deleted file mode 100644 index ee2b3fd286..0000000000 --- a/packages/webui/src/client/ui/Settings/Studio/PackageManager.tsx +++ /dev/null @@ -1,846 +0,0 @@ -import ClassNames from 'classnames' -import * as React from 'react' -import { Meteor } from 'meteor/meteor' -import * as _ from 'underscore' -import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { EditAttribute, EditAttributeBase } from '../../../lib/EditAttribute' -import { doModalDialog } from '../../../lib/ModalDialog' -import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTrash, faPencilAlt, faCheck, faPlus } from '@fortawesome/free-solid-svg-icons' -import { withTranslation } from 'react-i18next' -import { Accessor } from '@sofie-automation/blueprints-integration' -import { Studios } from '../../../collections' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { LabelActual } from '../../../lib/Components/LabelAndOverrides' - -interface IStudioPackageManagerSettingsProps { - studio: DBStudio -} -interface IStudioPackageManagerSettingsState { - editedPackageContainer: Array - editedAccessors: Array -} - -export const StudioPackageManagerSettings = withTranslation()( - class StudioPackageManagerSettings extends React.Component< - Translated, - IStudioPackageManagerSettingsState - > { - constructor(props: Translated) { - super(props) - - this.state = { - editedPackageContainer: [], - editedAccessors: [], - } - } - isPackageContainerEdited = (containerId: string) => { - return this.state.editedPackageContainer.indexOf(containerId) >= 0 - } - finishEditPackageContainer = (containerId: string) => { - const index = this.state.editedPackageContainer.indexOf(containerId) - if (index >= 0) { - this.state.editedPackageContainer.splice(index, 1) - this.setState({ - editedPackageContainer: this.state.editedPackageContainer, - }) - } - } - editPackageContainer = (containerId: string) => { - if (this.state.editedPackageContainer.indexOf(containerId) < 0) { - this.state.editedPackageContainer.push(containerId) - this.setState({ - editedPackageContainer: this.state.editedPackageContainer, - }) - } else { - this.finishEditPackageContainer(containerId) - } - } - confirmRemovePackageContainer = (containerId: string) => { - const { t } = this.props - doModalDialog({ - title: t('Remove this Package Container?'), - yes: t('Remove'), - no: t('Cancel'), - onAccept: () => { - this.removePackageContainer(containerId) - }, - message: ( - -

- {t('Are you sure you want to remove the Package Container "{{containerId}}"?', { - containerId: containerId, - })} -

-

{t('Please note: This action is irreversible!')}

-
- ), - }) - } - removePackageContainer = (containerId: string) => { - const unsetObject: Record = {} - unsetObject['packageContainers.' + containerId] = 1 - Studios.update(this.props.studio._id, { - $unset: unsetObject, - }) - } - addNewPackageContainer = () => { - // find free key name - const newKeyName = 'newContainer' - let iter = 0 - while ((this.props.studio.packageContainers || {})[newKeyName + iter]) { - iter++ - } - - const newPackageContainer: StudioPackageContainer = { - deviceIds: [], - container: { - label: 'New Package Container', - accessors: {}, - }, - } - const setObject: Record = {} - setObject['packageContainers.' + newKeyName + iter] = newPackageContainer - - Studios.update(this.props.studio._id, { - $set: setObject, - }) - } - containerId = (edit: EditAttributeBase, newValue: string) => { - const oldContainerId = edit.props.overrideDisplayValue - const newContainerId = newValue + '' - const packageContainer = this.props.studio.packageContainers[oldContainerId] - - if (this.props.studio.packageContainers[newContainerId]) { - throw new Meteor.Error(400, 'PackageContainer "' + newContainerId + '" already exists') - } - - const mSet: Record = {} - const mUnset: Record = {} - mSet['packageContainers.' + newContainerId] = packageContainer - mUnset['packageContainers.' + oldContainerId] = 1 - - if (edit.props.collection) { - edit.props.collection.update(this.props.studio._id, { - $set: mSet, - $unset: mUnset, - }) - } - - this.finishEditPackageContainer(oldContainerId) - this.editPackageContainer(newContainerId) - } - getPlayoutDeviceIds() { - const deviceIds: { - name: string - value: string - }[] = [] - - const playoutDevices = applyAndValidateOverrides(this.props.studio.peripheralDeviceSettings.playoutDevices).obj - - for (const deviceId of Object.keys(playoutDevices)) { - deviceIds.push({ - name: deviceId, - value: deviceId, - }) - } - - return deviceIds - } - renderPackageContainers() { - const { t } = this.props - - if (Object.keys(this.props.studio.packageContainers).length === 0) { - return ( - - {t('There are no Package Containers set up.')} - - ) - } - - return _.map( - this.props.studio.packageContainers, - (packageContainer: StudioPackageContainer, containerId: string) => { - return ( - - - {containerId} - {packageContainer.container.label} - - - - - - - {this.isPackageContainerEdited(containerId) && ( - - -
- - - - - - -
-
-
-
-

{t('Accessors')}

- - {this.renderAccessors(containerId, packageContainer)} -
-
- -
-
-
- - - )} -
- ) - } - ) - } - isAccessorEdited = (containerId: string, accessorId: string) => { - return this.state.editedAccessors.indexOf(containerId + accessorId) >= 0 - } - finishEditAccessor = (containerId: string, accessorId: string) => { - const index = this.state.editedAccessors.indexOf(containerId + accessorId) - if (index >= 0) { - this.state.editedAccessors.splice(index, 1) - this.setState({ - editedAccessors: this.state.editedAccessors, - }) - } - } - editAccessor = (containerId: string, accessorId: string) => { - if (this.state.editedAccessors.indexOf(containerId + accessorId) < 0) { - this.state.editedAccessors.push(containerId + accessorId) - this.setState({ - editedAccessors: this.state.editedAccessors, - }) - } else { - this.finishEditAccessor(containerId, accessorId) - } - } - confirmRemoveAccessor = (containerId: string, accessorId: string) => { - const { t } = this.props - doModalDialog({ - title: t('Remove this Package Container Accessor?'), - yes: t('Remove'), - no: t('Cancel'), - onAccept: () => { - this.removeAccessor(containerId, accessorId) - }, - message: ( - -

- {t('Are you sure you want to remove the Package Container Accessor "{{accessorId}}"?', { - accessorId: accessorId, - })} -

-

{t('Please note: This action is irreversible!')}

-
- ), - }) - } - removeAccessor = (containerId: string, accessorId: string) => { - const unsetObject: Record = {} - unsetObject[`packageContainers.${containerId}.container.accessors.${accessorId}`] = 1 - Studios.update(this.props.studio._id, { - $unset: unsetObject, - }) - } - addNewAccessor = (containerId: string) => { - // find free key name - const newKeyName = 'local' - let iter = 0 - const packageContainer = this.props.studio.packageContainers[containerId] - if (!packageContainer) throw new Error(`Can't add an accessor to nonexistant Package Container "${containerId}"`) - - while (packageContainer.container.accessors[newKeyName + iter]) { - iter++ - } - const accessorId = newKeyName + iter - - const newAccessor: Accessor.LocalFolder = { - type: Accessor.AccessType.LOCAL_FOLDER, - label: 'Local folder', - allowRead: true, - allowWrite: false, - folderPath: '', - } - const setObject: Record = {} - setObject[`packageContainers.${containerId}.container.accessors.${accessorId}`] = newAccessor - - Studios.update(this.props.studio._id, { - $set: setObject, - }) - } - updateAccessorId = (edit: EditAttributeBase, newValue: string) => { - const oldAccessorId = edit.props.overrideDisplayValue - const newAccessorId = newValue + '' - const containerId = edit.props.attribute - if (!containerId) throw new Error(`containerId not set`) - const packageContainer = this.props.studio.packageContainers[containerId] - if (!packageContainer) throw new Error(`Can't edit an accessor to nonexistant Package Container "${containerId}"`) - - const accessor = this.props.studio.packageContainers[containerId].container.accessors[oldAccessorId] - - if (this.props.studio.packageContainers[containerId].container.accessors[newAccessorId]) { - throw new Meteor.Error(400, 'Accessor "' + newAccessorId + '" already exists') - } - - const mSet: Record = {} - const mUnset: Record = {} - mSet[`packageContainers.${containerId}.container.accessors.${newAccessorId}`] = accessor - mUnset[`packageContainers.${containerId}.container.accessors.${oldAccessorId}`] = 1 - - if (edit.props.collection) { - edit.props.collection.update(this.props.studio._id, { - $set: mSet, - $unset: mUnset, - }) - } - - this.finishEditAccessor(containerId, oldAccessorId) - this.editAccessor(containerId, newAccessorId) - } - - renderAccessors(containerId: string, packageContainer: StudioPackageContainer) { - const { t } = this.props - - if (Object.keys(this.props.studio.packageContainers).length === 0) { - return ( - - {t('There are no Accessors set up.')} - - ) - } - - return _.map(packageContainer.container.accessors, (accessor: Accessor.Any, accessorId: string) => { - const accessorContent: string[] = [] - _.each(accessor as any, (value, key: string) => { - if (key !== 'type' && value !== '') { - let str = JSON.stringify(value) - if (str.length > 20) str = str.slice(0, 17) + '...' - accessorContent.push(`${key}: ${str}`) - } - }) - return ( - - - {accessorId} - {/* {accessor.name} */} - {accessor.type} - {accessorContent.join(', ')} - - - - - - - {this.isAccessorEdited(containerId, accessorId) && ( - - -
- - - - {accessor.type === Accessor.AccessType.LOCAL_FOLDER ? ( - <> - - - - - ) : accessor.type === Accessor.AccessType.HTTP ? ( - <> - - - - - - - ) : accessor.type === Accessor.AccessType.HTTP_PROXY ? ( - <> - - - - - ) : accessor.type === Accessor.AccessType.FILE_SHARE ? ( - <> - - - - - - ) : accessor.type === Accessor.AccessType.QUANTEL ? ( - <> - - - - - - - - - - - - ) : null} - - - - -
-
- -
- - - )} -
- ) - }) - } - getAvailablePackageContainers() { - const arr: { - name: string - value: string - }[] = [] - - for (const [containerId, packageContainer] of Object.entries( - this.props.studio.packageContainers - )) { - let hasHttpAccessor = false - for (const accessor of Object.values(packageContainer.container.accessors)) { - if (accessor.type === Accessor.AccessType.HTTP_PROXY) { - hasHttpAccessor = true - break - } - } - if (hasHttpAccessor) { - arr.push({ - name: packageContainer.container.label, - value: containerId, - }) - } - } - return arr - } - - render(): JSX.Element { - const { t } = this.props - return ( -
-

{t('Package Manager')}

- -
-

{t('Studio Settings')}

- -
-
- -
- -
-
-
- -
- -
-
-
- -

{t('Package Containers')}

- - {this.renderPackageContainers()} -
-
- -
-
-
- ) - } - } -) diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTable.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTable.tsx new file mode 100644 index 0000000000..f34a59683c --- /dev/null +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTable.tsx @@ -0,0 +1,77 @@ +import * as React from 'react' +import * as _ from 'underscore' +import { StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faPlus } from '@fortawesome/free-solid-svg-icons' +import { useTranslation } from 'react-i18next' +import { Accessor } from '@sofie-automation/blueprints-integration' +import { useToggleExpandHelper } from '../../../util/useToggleExpandHelper' +import { OverrideOpHelper, WrappedOverridableItemNormal } from '../../util/OverrideOpHelper' +import { AccessorTableRow } from './AccessorTableRow' + +interface AccessorsTableProps { + packageContainer: WrappedOverridableItemNormal + overrideHelper: OverrideOpHelper +} + +export function AccessorsTable({ packageContainer, overrideHelper }: AccessorsTableProps): React.JSX.Element { + const { t } = useTranslation() + const { toggleExpanded, isExpanded } = useToggleExpandHelper() + + const addNewAccessor = React.useCallback(() => { + const newKeyName = 'local' + let iter = 0 + if (!packageContainer.id) + throw new Error(`Can't add an accessor to nonexistant Package Container "${packageContainer.id}"`) + + while (packageContainer.computed?.container.accessors[newKeyName + iter]) { + iter++ + } + const accessorId = newKeyName + iter + + const newAccessor: Accessor.LocalFolder = { + type: Accessor.AccessType.LOCAL_FOLDER, + label: 'Local folder', + allowRead: true, + allowWrite: false, + folderPath: '', + } + + overrideHelper().setItemValue(packageContainer.id, `container.accessors.${accessorId}`, newAccessor).commit() + + setTimeout(() => { + toggleExpanded(accessorId, true) + }, 1) + }, [toggleExpanded, overrideHelper]) + + const container = packageContainer.computed.container + + return ( + <> + + {Object.keys(container.accessors || {}).length === 0 ? ( + + + + ) : ( + _.map(container.accessors || {}, (accessor: Accessor.Any, accessorId: string) => ( + + )) + )} +
{t('There are no Accessors set up.')}
+
+ +
+ + ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx new file mode 100644 index 0000000000..eee92146cd --- /dev/null +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx @@ -0,0 +1,565 @@ +import ClassNames from 'classnames' +import * as React from 'react' +import { Meteor } from 'meteor/meteor' +import { StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { doModalDialog } from '../../../../lib/ModalDialog' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTrash, faPencilAlt, faCheck } from '@fortawesome/free-solid-svg-icons' +import { useTranslation } from 'react-i18next' +import { Accessor } from '@sofie-automation/blueprints-integration' +import { + LabelActual, + LabelAndOverrides, + LabelAndOverridesForCheckbox, + LabelAndOverridesForDropdown, +} from '../../../../lib/Components/LabelAndOverrides' +import { TextInputControl } from '../../../../lib/Components/TextInput' +import { DropdownInputControl, getDropdownInputOptions } from '../../../../lib/Components/DropdownInput' +import { OverrideOpHelper, WrappedOverridableItemNormal } from '../../util/OverrideOpHelper' +import { CheckboxControl } from '../../../../lib/Components/Checkbox' + +interface AccessorTableRowProps { + packageContainer: WrappedOverridableItemNormal + accessorId: string + accessor: Accessor.Any + overrideHelper: OverrideOpHelper + toggleExpanded: (exclusivityGroupId: string, force?: boolean) => void + isExpanded: boolean +} + +export function AccessorTableRow({ + accessor, + accessorId, + packageContainer, + overrideHelper, + toggleExpanded, + isExpanded, +}: AccessorTableRowProps): React.JSX.Element { + const { t } = useTranslation() + + const confirmRemoveAccessor = (accessorId: string) => { + doModalDialog({ + title: t('Remove this Package Container Accessor?'), + yes: t('Remove'), + no: t('Cancel'), + onAccept: () => { + overrideHelper().setItemValue(packageContainer.id, `container.accessors.${accessorId}`, undefined).commit() + }, + message: ( + +

+ {t('Are you sure you want to remove the Package Container Accessor "{{accessorId}}"?', { + accessorId: accessorId, + })} +

+

{t('Please note: This action is irreversible!')}

+
+ ), + }) + } + + const updateAccessorId = React.useCallback( + (newAccessorId: string) => { + const oldAccessorId = accessorId + if (!packageContainer.id) throw new Error(`containerId not set`) + if (!packageContainer) throw new Error(`Can't edit an accessor to nonexistant Package Container"`) + + const accessor = packageContainer.computed?.container.accessors[oldAccessorId] + + if (packageContainer.computed?.container.accessors[newAccessorId]) { + throw new Meteor.Error(400, 'Accessor "' + newAccessorId + '" already exists') + } + + // Add a copy of accessor with the new ID, and remove the old + overrideHelper() + .setItemValue(packageContainer.id, `container.accessors.${oldAccessorId}`, undefined) + .setItemValue(packageContainer.id, `container.accessors.${newAccessorId}`, accessor) + .commit() + + setTimeout(() => { + toggleExpanded(oldAccessorId, false) + toggleExpanded(newAccessorId, true) + }, 100) + }, + [overrideHelper, toggleExpanded, packageContainer, accessorId] + ) + + if (Object.keys(packageContainer.computed?.container || {}).length === 0) { + return ( + + {t('There are no Accessors set up.')} + + ) + } + + return ( + + + {accessorId} + {/* {accessor.name} */} + {accessor.label} + {/*{accessorContent.join(', ')}*/} + + + + + + + {isExpanded && ( + + +
+ + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate, options) => { + return ( + + ) + }} + + {accessor.type === Accessor.AccessType.LOCAL_FOLDER ? ( + <> + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + ) : accessor.type === Accessor.AccessType.HTTP ? ( + <> + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + ) : accessor.type === Accessor.AccessType.HTTP_PROXY ? ( + <> + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + ) : accessor.type === Accessor.AccessType.FILE_SHARE ? ( + <> + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + ) : accessor.type === Accessor.AccessType.QUANTEL ? ( + <> + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate) => ( + + )} + + + ) : null} + + + {(value, handleUpdate) => } + + + {(value, handleUpdate) => } + +
+
+ +
+ + + )} +
+ ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx new file mode 100644 index 0000000000..576425b299 --- /dev/null +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { EditAttribute } from '../../../../lib/EditAttribute' +import { useTranslation } from 'react-i18next' +import { Accessor } from '@sofie-automation/blueprints-integration' +import { Studios } from '../../../../collections' +import { DropdownInputOption } from '../../../../lib/Components/DropdownInput' +import { WrappedOverridableItem } from '../../util/OverrideOpHelper' + +interface PackageContainersPickersProps { + studio: DBStudio + packageContainersFromOverrides: WrappedOverridableItem[] +} + +export function PackageContainersPickers({ + studio, + packageContainersFromOverrides, +}: PackageContainersPickersProps): JSX.Element { + const { t } = useTranslation() + + const availablePackageContainerOptions = React.useMemo(() => { + const arr: DropdownInputOption[] = [] + + packageContainersFromOverrides.forEach((packageContainer) => { + let hasHttpAccessor = false + if (packageContainer.computed) { + for (const accessor of Object.values(packageContainer.computed.container.accessors)) { + if (accessor.type === Accessor.AccessType.HTTP_PROXY) { + hasHttpAccessor = true + break + } + } + if (hasHttpAccessor) { + arr.push({ + name: packageContainer.computed.container.label, + value: packageContainer.id, + i: arr.length, + }) + } + } + }) + return arr + }, [packageContainersFromOverrides]) + + return ( +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx new file mode 100644 index 0000000000..d92e3e31c8 --- /dev/null +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx @@ -0,0 +1,331 @@ +import ClassNames from 'classnames' +import * as React from 'react' +import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { doModalDialog } from '../../../../lib/ModalDialog' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTrash, faPencilAlt, faPlus, faSync } from '@fortawesome/free-solid-svg-icons' +import { useTranslation } from 'react-i18next' +import { Studios } from '../../../../collections' +import { + ObjectOverrideSetOp, + SomeObjectOverrideOp, + applyAndValidateOverrides, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { + LabelActual, + LabelAndOverrides, + LabelAndOverridesForMultiSelect, +} from '../../../../lib/Components/LabelAndOverrides' +import { useToggleExpandHelper } from '../../../util/useToggleExpandHelper' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { TextInputControl } from '../../../../lib/Components/TextInput' +import { DropdownInputOption } from '../../../../lib/Components/DropdownInput' +import { MultiSelectInputControl } from '../../../../lib/Components/MultiSelectInput' +import { + OverrideOpHelper, + WrappedOverridableItem, + WrappedOverridableItemNormal, + useOverrideOpHelper, +} from '../../util/OverrideOpHelper' +import { AccessorsTable } from './AccessorTable' + +interface PackageContainersTableProps { + studio: DBStudio + packageContainersFromOverrides: WrappedOverridableItem[] +} + +export function PackageContainersTable({ + studio, + packageContainersFromOverrides, +}: PackageContainersTableProps): React.JSX.Element { + const { t } = useTranslation() + const { toggleExpanded, isExpanded } = useToggleExpandHelper() + + const saveOverrides = React.useCallback( + (newOps: SomeObjectOverrideOp[]) => { + Studios.update(studio._id, { + $set: { + 'packageContainersWithOverrides.overrides': newOps, + }, + }) + }, + [studio._id] + ) + + const overrideHelper = useOverrideOpHelper(saveOverrides, studio.packageContainersWithOverrides) + + const addNewPackageContainer = React.useCallback(() => { + const resolvedPackageContainers = applyAndValidateOverrides(studio.packageContainersWithOverrides).obj + + // find free key name + const newKeyName = 'newContainer' + let iter = 0 + while (resolvedPackageContainers[newKeyName + iter.toString()]) { + iter++ + } + + const newId = newKeyName + iter.toString() + const newPackageContainer: StudioPackageContainer = { + deviceIds: [], + container: { + label: 'New Package Container ' + iter.toString(), + accessors: {}, + }, + } + + const addOp = literal({ + op: 'set', + path: newId, + value: newPackageContainer, + }) + + Studios.update(studio._id, { + $push: { + 'packageContainersWithOverrides.overrides': addOp, + }, + }) + + setTimeout(() => { + toggleExpanded(newId, true) + }, 1) + }, [studio._id, studio.packageContainersWithOverrides]) + + const confirmRemovePackageContainer = (containerId: string) => { + doModalDialog({ + title: t('Remove this Package Container?'), + yes: t('Remove'), + no: t('Cancel'), + onAccept: () => { + overrideHelper().deleteItem(containerId).commit() + }, + message: ( + +

+ {t('Are you sure you want to remove the Package Container "{{containerId}}"?', { + containerId: containerId, + })} +

+

{t('Please note: This action is irreversible!')}

+
+ ), + }) + } + + const confirmReset = React.useCallback( + (packgageContainerId: string) => { + doModalDialog({ + title: t('Reset this Package Container?'), + yes: t('Reset'), + no: t('Cancel'), + onAccept: () => { + overrideHelper().resetItem(packgageContainerId).commit() + }, + message: ( + +

+ {t('Are you sure you want to reset all overrides for Packing Container "{{id}}"?', { + id: packgageContainerId, + })} +

+

{t('Please note: This action is irreversible!')}

+
+ ), + }) + }, + [t, packageContainersFromOverrides, overrideHelper] + ) + + return ( + <> + + {packageContainersFromOverrides.map( + (packageContainer: WrappedOverridableItem): React.JSX.Element => + packageContainer.type == 'normal' ? ( + + ) : ( + + ) + )} +
+
+ +
+ + ) +} + +interface PackageContainerDeletedRowProps { + packageContainer: WrappedOverridableItem + overrideHelper: OverrideOpHelper +} + +function PackageContainerDeletedRow({ packageContainer, overrideHelper }: Readonly) { + const doUndeleteItem = React.useCallback( + () => overrideHelper().resetItem(packageContainer.id).commit(), + [overrideHelper, packageContainer.id] + ) + + return ( + + {packageContainer.id} + {packageContainer.defaults?.container.label} + {packageContainer.id} + + + + + ) +} + +interface PackageContainerRowProps { + studio: DBStudio + packageContainer: WrappedOverridableItemNormal + overrideHelper: OverrideOpHelper + toggleExpanded: (id: string, forceState?: boolean | undefined) => void + isExpanded: (id: string) => boolean + confirmRemovePackageContainer: (id: string) => void + confirmReset: (id: string) => void +} + +function PackageContainerRow({ + studio, + packageContainer, + overrideHelper, + toggleExpanded, + isExpanded, + confirmRemovePackageContainer, + confirmReset, +}: PackageContainerRowProps): React.JSX.Element { + const { t } = useTranslation() + + const availablePlayoutDevicesOptions: DropdownInputOption[] = React.useMemo(() => { + const playoutDevicesFromOverrrides = applyAndValidateOverrides(studio.peripheralDeviceSettings.playoutDevices).obj + + const devices: DropdownInputOption[] = [] + + for (const deviceId of Object.keys(playoutDevicesFromOverrrides)) { + devices.push({ + name: deviceId, + value: deviceId, + i: devices.length, + }) + } + return devices + }, [studio.peripheralDeviceSettings.playoutDevices]) + + const updatePackageContainerId = React.useCallback( + (newPackageContainerId: string) => { + overrideHelper().changeItemId(packageContainer.id, newPackageContainerId).commit() + toggleExpanded(newPackageContainerId, true) + }, + [overrideHelper, toggleExpanded, packageContainer.id] + ) + + return ( + + + {packageContainer.id} + {packageContainer.computed.container.label} + + + {packageContainer.defaults && packageContainer.overrideOps.length > 0 && ( + + )} + + + + + {isExpanded(packageContainer.id) && ( + + +
+ + + {(value, handleUpdate) => ( + + )} + + + {(value, handleUpdate, options) => ( + + )} + +
+
+
+
+

{t('Accessors')}

+ +
+
+ + + )} +
+ ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/index.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/index.tsx new file mode 100644 index 0000000000..707b1372f9 --- /dev/null +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/index.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { useTranslation } from 'react-i18next' +import { getAllCurrentAndDeletedItemsFromOverrides } from '../../util/OverrideOpHelper' +import { PackageContainersPickers } from './PackageContainerPickers' +import { PackageContainersTable } from './PackageContainers' + +interface StudioPackageManagerSettingsProps { + studio: DBStudio +} + +export function StudioPackageManagerSettings({ studio }: StudioPackageManagerSettingsProps): React.JSX.Element { + const { t } = useTranslation() + + const packageContainersFromOverrides = React.useMemo( + () => + getAllCurrentAndDeletedItemsFromOverrides(studio.packageContainersWithOverrides, (a, b) => + a[0].localeCompare(b[0]) + ), + [studio.packageContainersWithOverrides] + ) + + return ( +
+

{t('Package Manager')}

+ +
+

{t('Studio Settings')}

+ + + +

{t('Package Containers')}

+ +
+
+ ) +}