From a7d69998085c05617c3e056327bb6cd203ebdd39 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Fri, 13 Dec 2024 19:08:53 +0100 Subject: [PATCH 01/15] fix(job-worker/playout): Hold mode doesn't work at all --- .../src/playout/model/PlayoutModel.ts | 5 ++ .../model/implementation/PlayoutModelImpl.ts | 10 +-- packages/job-worker/src/playout/take.ts | 84 ++++++++++--------- .../src/playout/timings/partPlayback.ts | 1 + 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 396a465373..3b944f9f3f 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -234,6 +234,11 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa */ cycleSelectedPartInstances(): void + /** + * Reset the hold state to a base state + */ + resetHoldState(): void + /** * Set the RundownPlaylist as deactivated */ diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index b2d0508a08..5b57a57152 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -420,15 +420,13 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.playlistImpl.nextPartInfo = null this.playlistImpl.lastTakeTime = getCurrentTime() - if (!this.playlistImpl.holdState || this.playlistImpl.holdState === RundownHoldState.COMPLETE) { - this.playlistImpl.holdState = RundownHoldState.NONE - } else { - this.playlistImpl.holdState = this.playlistImpl.holdState + 1 - } - this.#playlistHasChanged = true } + resetHoldState(): void { + this.setHoldState(RundownHoldState.NONE) + } + deactivatePlaylist(): void { delete this.playlistImpl.activationId diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index f4a404d993..c089685d8d 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -178,20 +178,23 @@ export async function performTakeToNextedPart( } } + // If hold is COMPLETE, clear the hold state by this take if (playoutModel.playlist.holdState === RundownHoldState.COMPLETE) { playoutModel.setHoldState(RundownHoldState.NONE) - // If hold is active, then this take is to clear it + // If hold is ACTIVE, then this take is to complete it } else if (playoutModel.playlist.holdState === RundownHoldState.ACTIVE) { await completeHold(context, playoutModel, await pShowStyle, currentPartInstance) + await updateTimeline(context, playoutModel) + if (span) span.end() return } const takePartInstance = nextPartInstance - if (!takePartInstance) throw new Error('takePart not found!') + if (!takePartInstance) throw new Error('takePartInstance not found!') const takeRundown = playoutModel.getRundown(takePartInstance.partInstance.rundownId) if (!takeRundown) throw new Error(`takeRundown: takeRundown not found! ("${takePartInstance.partInstance.rundownId}")`) @@ -263,12 +266,10 @@ export async function performTakeToNextedPart( // Once everything is synced, we can choose the next part await setNextPart(context, playoutModel, nextPart, false) - // Setup the parts for the HOLD we are starting - if ( - playoutModel.playlist.previousPartInfo && - (playoutModel.playlist.holdState as RundownHoldState) === RundownHoldState.ACTIVE - ) { - startHold(context, currentPartInstance, nextPartInstance) + // If the Hold is PENDING, make it active + if (playoutModel.playlist.holdState === RundownHoldState.PENDING) { + // Setup the parts for the HOLD we are starting + activateHold(context, playoutModel, currentPartInstance, takePartInstance) } await afterTake(context, playoutModel, takePartInstance) @@ -535,35 +536,39 @@ export async function afterTake( /** * A Hold starts by extending the "extendOnHold"-able pieces in the previous Part. */ -function startHold( +function activateHold( context: JobContext, + playoutModel: PlayoutModel, holdFromPartInstance: PlayoutPartInstanceModel | null, holdToPartInstance: PlayoutPartInstanceModel | undefined ) { if (!holdFromPartInstance) throw new Error('previousPart not found!') if (!holdToPartInstance) throw new Error('currentPart not found!') - const span = context.startSpan('startHold') + const span = context.startSpan('activateHold') + + playoutModel.setHoldState(RundownHoldState.ACTIVE) // Make a copy of any item which is flagged as an 'infinite' extension const pieceInstancesToCopy = holdFromPartInstance.pieceInstances.filter((p) => !!p.pieceInstance.piece.extendOnHold) - pieceInstancesToCopy.forEach((instance) => { - if (!instance.pieceInstance.infinite) { - // mark current one as infinite - instance.prepareForHold() - - // This gets deleted once the nextpart is activated, so it doesnt linger for long - const extendedPieceInstance = holdToPartInstance.insertHoldPieceInstance(instance) - - const content = clone(instance.pieceInstance.piece.content) as VTContent | undefined - if (content?.fileName && content.sourceDuration && instance.pieceInstance.plannedStartedPlayback) { - content.seek = Math.min( - content.sourceDuration, - getCurrentTime() - instance.pieceInstance.plannedStartedPlayback - ) - } - extendedPieceInstance.updatePieceProps({ content }) + for (const instance of pieceInstancesToCopy) { + // skip any infinites + if (instance.pieceInstance.infinite) continue + + instance.prepareForHold() + + // This gets deleted once the nextpart is activated, so it doesnt linger for long + const extendedPieceInstance = holdToPartInstance.insertHoldPieceInstance(instance) + + const content = clone(instance.pieceInstance.piece.content) as VTContent | undefined + if (content?.fileName && content.sourceDuration && instance.pieceInstance.plannedStartedPlayback) { + content.seek = Math.min( + content.sourceDuration, + getCurrentTime() - instance.pieceInstance.plannedStartedPlayback + ) } - }) + extendedPieceInstance.updatePieceProps({ content }) + } + if (span) span.end() } @@ -575,19 +580,16 @@ async function completeHold( ): Promise { playoutModel.setHoldState(RundownHoldState.COMPLETE) - if (playoutModel.playlist.currentPartInfo) { - if (!currentPartInstance) throw new Error('currentPart not found!') + if (!playoutModel.playlist.currentPartInfo) return + if (!currentPartInstance) throw new Error('currentPart not found!') - // Clear the current extension line - innerStopPieces( - context, - playoutModel, - showStyleCompound.sourceLayers, - currentPartInstance, - (p) => !!p.infinite?.fromHold, - undefined - ) - } - - await updateTimeline(context, playoutModel) + // Clear the current extension line + innerStopPieces( + context, + playoutModel, + showStyleCompound.sourceLayers, + currentPartInstance, + (p) => !!p.infinite?.fromHold, + undefined + ) } diff --git a/packages/job-worker/src/playout/timings/partPlayback.ts b/packages/job-worker/src/playout/timings/partPlayback.ts index 8c3fc07d16..279d1dd410 100644 --- a/packages/job-worker/src/playout/timings/partPlayback.ts +++ b/packages/job-worker/src/playout/timings/partPlayback.ts @@ -58,6 +58,7 @@ export async function onPartPlaybackStarted( // this is the next part, clearly an autoNext has taken place playoutModel.cycleSelectedPartInstances() + playoutModel.resetHoldState() reportPartInstanceHasStarted(context, playoutModel, playingPartInstance, data.startedPlayback) From 0399c5645d50de191a83954f5a9375954a1b84a4 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 11 Dec 2024 13:47:35 +0000 Subject: [PATCH 02/15] chore: add docs for distributed package-manager --- .../installing-package-manager.md | 110 ++++++++++++++++-- 1 file changed, 103 insertions(+), 7 deletions(-) diff --git a/packages/documentation/docs/user-guide/installation/installing-package-manager.md b/packages/documentation/docs/user-guide/installation/installing-package-manager.md index bd6cbf3a15..71f52704eb 100644 --- a/packages/documentation/docs/user-guide/installation/installing-package-manager.md +++ b/packages/documentation/docs/user-guide/installation/installing-package-manager.md @@ -18,7 +18,14 @@ Although Package Manager can be used to copy any kind of file to/from a wide arr :::caution -At this time, the Package Manager worker process is Windows-only. Therefore, these instructions as a whole will only work on Windows. The worker will not work on WSL2. +Sofie supports only one package manager running for a Studio. Attaching more at a time will result in weird behaviour due to them fighting over reporting the statuses of packages. +If you feel like you need multiple, then you likely want to run package manager in the split setup instead. + +::: + +:::caution + +The package manager worker process is primarily tested on Windows only. It does run on Linux (without support for network shares), but has not been extensively tested. ::: @@ -49,10 +56,13 @@ To setup, go into Core and add this device to a Studio This first run is necessary to get the Package Manager device registered with _Sofie Core_. We'll restart Package Manager later on in the [Configuration](#configuration) instructions. - ## Installation In Production -We provide pre-built executables for Windows (x64) systems that can be used in production environments. These can be found on the [Releases](https://github.com/nrkno/sofie-package-manager/releases) GitHub repository page for Package Manager. For a minimal installation, you'll need the `package-manager-single-app.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. +Only one package manager can be running for a Sofie Studio. If you reached this point thinking of deploying multiple, you will want to follow the distributed setup. + +### Simple setup + +For setups where you only need to interact with CasparCG on one machine, we provide pre-built executables for Windows (x64) systems. These can be found on the [Releases](https://github.com/nrkno/sofie-package-manager/releases) GitHub repository page for Package Manager. For a minimal installation, you'll need the `package-manager-single-app.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. ```bash package-manager-single-app.exe --coreHost= --corePort= --deviceId= --deviceToken= @@ -62,22 +72,108 @@ Package Manager can be launched from [CasparCG Launcher](./installing-connection You can see a list of available options by running `package-manager-single-app.exe --help`. +In some cases, you will need to run the http proxy server component elsewhere so that it can be accessed from your Sofie UI machines. +For this, you can run the `sofietv/package-manager-http-server` docker image, which exposes its service on port 8080 and expects `/data/http-server` to be persistent storage. +When configuring the http proxy server in sofie, you may need to follow extra configuration steps for this to work as expected. + +### Distributed setup + +For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, package manager can be partially deployed in docker, with just the workers. + +An example docker-compose of the setup is as follows: + +``` +services: + http-server: + build: + context: . + dockerfile: sofietv/package-manager-http-server + environment: + HTTP_SERVER_BASE_PATH: '/data/http-server' + ports: + - '8080:8080' + volumes: + - http-server-data:/data/http-server + + workforce: + build: + context: . + dockerfile: sofietv/package-manager-workforce + ports: + - '8070:8070' # this needs to be exposed so that the workers can connect back to it + + package-manager: + depends_on: + - http-server + - workforce + build: + context: . + dockerfile: sofietv/package-manager-package-manager + environment: + CORE_HOST: '172.18.0.1' # the address for connecting back to sofie core from this image + CORE_PORT: '3000' + DEVICE_ID: 'my-package-manager-id' + DEVICE_TOKEN: 'some-secret' + WORKFORCE_URL: 'ws://workforce:8070' # referencing the workforce component above + PACKAGE_MANAGER_PORT: '8060' + PACKAGE_MANAGER_URL: 'ws://insert-service-ip-here:8060' # the workers connect back to this address, so it needs to be accessible from casparcg + # CONCURRENCY: 10 # How many expectation states can be evaluated at the same time + ports: + - '8060:8060' + +networks: + default: +volumes: + http-server-data: +``` + +In addition to this, you will need to run the appContainer and workers on each windows machine that package-manager needs access to: + +``` +./appContainer-node.exe + --appContainerId=caspar01 // This is a unique id for this instance of the appContainer + --workforceURL=ws://workforce-service-ip:8070 + --resourceId=caspar01 // This should also be set in the 'resource id' field of the `casparcgLocalFolder1` accessor. This is how package manager can identify which machine is which. + --networkIds=pm-net // This is not necessary, but can be useful for more complex setups +``` + +You can get the windows executables from [Releases](https://github.com/nrkno/sofie-package-manager/releases) GitHub repository page for Package Manager. You'll need the `appContainer-node.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. + +Note that each appContainer needs to use a different resourceId and will need its own package containers set to use the same resourceIds if they need to access the local disk. This is how package-manager knows which workers have access to which machines. + ## Configuration -1. Open the _Sofie Core_ Settings page ([http://localhost:3000/settings?admin=1](http://localhost:3000/settings?admin=1)), click on your Studio, and scroll down to the Attached Devices section. -1. Click the plus button (`+`) and select Package Manager to add the Package Manager device to your Studio. -1. On this same settings page, scroll down to the Package Manager section. +1. Open the _Sofie Core_ Settings page ([http://localhost:3000/settings?admin=1](http://localhost:3000/settings?admin=1)), click on your Studio, and then Peripheral Devices. +1. Click the plus button (`+`) in the Parent Devices section and configure the created device to be for your Package manager. +1. On the sidebar under the current Studio, select to the Package Manager section. 1. Click the plus button under the Package Containers heading, then click the edit icon (pencil) to the right of the newly-created package container. 1. Give this package container an ID of `casparcgContainer0` and a label of `CasparCG Package Container`. 1. Click on the dropdown under "Playout devices which use this package container" and select `casparcg0`. - If you don't have a `casparcg0` device, add it to the Playout Gateway under the Devices heading, then restart the Playout Gateway. + - If you are using the distributed setup, you will likely want to repeat this step for each casparcg machine. You will also want to set `Resource Id` to match the `resourceId` value provided in the appContainer command line. 1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. 1. Give this accessor an ID of `casparcgHttpProxy0`, a Label of `CasparCG HTTP Proxy Accessor`, an Accessor Type of `HTTP_PROXY`, and a Base URL of `http://localhost:8080/package`. Then, ensure that both the "Allow Read access" and "Allow Write access" boxes are checked. Finally, click the done button (checkmark icon) in the bottom right. -1. Scroll back up a bit to the "Studio Settings" subsection (still in the Package Manager section) and select "CasparCG Package Container" for both "Package Containers to use for previews" and "Package Containers to use for thumbnails". +1. Scroll back to the top of the page and select "CasparCG Package Container" for both "Package Containers to use for previews" and "Package Containers to use for thumbnails". 1. Your settings should look like this once all the above steps have been completed: ![Package Manager demo settings](/img/docs/Package_Manager_demo_settings.png) 1. If Package Manager `start:single-app` is running, restart it. If not, start it (see the above [Installation instructions](#installation-quick-start) for the relevant command line). +### Separate http proxy server + +In some setups, the url of the http proxy server is different when accessing the sofie ui and package manager. +You can use the 'Network ID' concept in package manager to provide guidance on which to use when. + +By adding `--networkIds=pm-net` (a semi colon separated list) when launching the exes on the CasparCG machine, the application will know to prefer certain accessors with matching values. + +Then in the Sofie UI: + +1. Return to the Package manager settings under the studio +1. Expand the `casparcgContainer` container. +1. Edit the `casparcgHttpProxy` accessor to have a `Base URL` that is accessible from the casparcg machines. +1. Set the `Network ID` to `pm-net` (matching what was passed in the command line) +1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. +1. Give this accessor an ID of `casparcgHttpProxyThumbnails0`, a Label of `CasparCG Thumbnail HTTP Proxy Accessor`, an Accessor Type of `HTTP_PROXY`, and a Base URL that is accessible to your Sofie client network. Then, ensure that only the "Allow Write access" box is checked. Finally, click the done button (checkmark icon) in the bottom right. + ## Usage In this basic configuration, Package Manager won't be copying any packages into your CasparCG Server media folder. Instead, it will simply check that the files in the rundown are present in your CasparCG Server media folder, and you'll have to manually place those files in the correct directory. However, thumbnail and preview generation will still function, as will status reporting. From 44b475eb894efa31b5994c616b35d00dc412bc54 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 16 Dec 2024 12:36:39 +0000 Subject: [PATCH 03/15] fix: package-manager publications forcing arguments --- packages/shared-lib/src/pubsub/peripheralDevice.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shared-lib/src/pubsub/peripheralDevice.ts b/packages/shared-lib/src/pubsub/peripheralDevice.ts index 9467b7794c..d170835290 100644 --- a/packages/shared-lib/src/pubsub/peripheralDevice.ts +++ b/packages/shared-lib/src/pubsub/peripheralDevice.ts @@ -103,16 +103,16 @@ export interface PeripheralDevicePubSubTypes { /** Custom publications for package-manager */ [PeripheralDevicePubSub.packageManagerPlayoutContext]: ( deviceId: PeripheralDeviceId, - token: string | undefined + token?: string ) => PeripheralDevicePubSubCollectionsNames.packageManagerPlayoutContext [PeripheralDevicePubSub.packageManagerPackageContainers]: ( deviceId: PeripheralDeviceId, - token: string | undefined + token?: string ) => PeripheralDevicePubSubCollectionsNames.packageManagerPackageContainers [PeripheralDevicePubSub.packageManagerExpectedPackages]: ( deviceId: PeripheralDeviceId, filterPlayoutDeviceIds: PeripheralDeviceId[] | undefined, - token: string | undefined + token?: string ) => PeripheralDevicePubSubCollectionsNames.packageManagerExpectedPackages } From b9d59c2529dd348add6399f5a230fbd379339094 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 16 Dec 2024 13:10:31 +0000 Subject: [PATCH 04/15] chore: update wording --- .../user-guide/installation/installing-package-manager.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/documentation/docs/user-guide/installation/installing-package-manager.md b/packages/documentation/docs/user-guide/installation/installing-package-manager.md index 71f52704eb..5a6ab9802b 100644 --- a/packages/documentation/docs/user-guide/installation/installing-package-manager.md +++ b/packages/documentation/docs/user-guide/installation/installing-package-manager.md @@ -78,7 +78,7 @@ When configuring the http proxy server in sofie, you may need to follow extra co ### Distributed setup -For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, package manager can be partially deployed in docker, with just the workers. +For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, package manager can be partially deployed in docker, with just the workers running on each CasparCG machine. An example docker-compose of the setup is as follows: @@ -116,7 +116,7 @@ services: DEVICE_TOKEN: 'some-secret' WORKFORCE_URL: 'ws://workforce:8070' # referencing the workforce component above PACKAGE_MANAGER_PORT: '8060' - PACKAGE_MANAGER_URL: 'ws://insert-service-ip-here:8060' # the workers connect back to this address, so it needs to be accessible from casparcg + PACKAGE_MANAGER_URL: 'ws://insert-service-ip-here:8060' # the workers connect back to this address, so it needs to be accessible from CasparCG # CONCURRENCY: 10 # How many expectation states can be evaluated at the same time ports: - '8060:8060' @@ -150,7 +150,7 @@ Note that each appContainer needs to use a different resourceId and will need it 1. Give this package container an ID of `casparcgContainer0` and a label of `CasparCG Package Container`. 1. Click on the dropdown under "Playout devices which use this package container" and select `casparcg0`. - If you don't have a `casparcg0` device, add it to the Playout Gateway under the Devices heading, then restart the Playout Gateway. - - If you are using the distributed setup, you will likely want to repeat this step for each casparcg machine. You will also want to set `Resource Id` to match the `resourceId` value provided in the appContainer command line. + - If you are using the distributed setup, you will likely want to repeat this step for each CasparCG machine. You will also want to set `Resource Id` to match the `resourceId` value provided in the appContainer command line. 1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. 1. Give this accessor an ID of `casparcgHttpProxy0`, a Label of `CasparCG HTTP Proxy Accessor`, an Accessor Type of `HTTP_PROXY`, and a Base URL of `http://localhost:8080/package`. Then, ensure that both the "Allow Read access" and "Allow Write access" boxes are checked. Finally, click the done button (checkmark icon) in the bottom right. 1. Scroll back to the top of the page and select "CasparCG Package Container" for both "Package Containers to use for previews" and "Package Containers to use for thumbnails". From aa3332a7f509d2705344325c7911b529addda514 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Dec 2024 10:07:54 +0000 Subject: [PATCH 05/15] chore: review comments Co-authored-by: Jan Starzak --- .../installing-package-manager.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/documentation/docs/user-guide/installation/installing-package-manager.md b/packages/documentation/docs/user-guide/installation/installing-package-manager.md index 5a6ab9802b..1f9b414d54 100644 --- a/packages/documentation/docs/user-guide/installation/installing-package-manager.md +++ b/packages/documentation/docs/user-guide/installation/installing-package-manager.md @@ -19,7 +19,7 @@ Although Package Manager can be used to copy any kind of file to/from a wide arr :::caution Sofie supports only one package manager running for a Studio. Attaching more at a time will result in weird behaviour due to them fighting over reporting the statuses of packages. -If you feel like you need multiple, then you likely want to run package manager in the split setup instead. +If you feel like you need multiple, then you likely want to run package manager in the distributed setup instead. ::: @@ -60,7 +60,7 @@ This first run is necessary to get the Package Manager device registered with _S Only one package manager can be running for a Sofie Studio. If you reached this point thinking of deploying multiple, you will want to follow the distributed setup. -### Simple setup +### Simple Setup For setups where you only need to interact with CasparCG on one machine, we provide pre-built executables for Windows (x64) systems. These can be found on the [Releases](https://github.com/nrkno/sofie-package-manager/releases) GitHub repository page for Package Manager. For a minimal installation, you'll need the `package-manager-single-app.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. @@ -72,15 +72,15 @@ Package Manager can be launched from [CasparCG Launcher](./installing-connection You can see a list of available options by running `package-manager-single-app.exe --help`. -In some cases, you will need to run the http proxy server component elsewhere so that it can be accessed from your Sofie UI machines. +In some cases, you will need to run the HTTP proxy server component elsewhere so that it can be accessed from your Sofie UI machines. For this, you can run the `sofietv/package-manager-http-server` docker image, which exposes its service on port 8080 and expects `/data/http-server` to be persistent storage. -When configuring the http proxy server in sofie, you may need to follow extra configuration steps for this to work as expected. +When configuring the http proxy server in Sofie, you may need to follow extra configuration steps for this to work as expected. -### Distributed setup +### Distributed Setup -For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, package manager can be partially deployed in docker, with just the workers running on each CasparCG machine. +For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, package manager can be partially deployed in Docker, with just the workers running on each CasparCG machine. -An example docker-compose of the setup is as follows: +An example `docker-compose` of the setup is as follows: ``` services: @@ -158,10 +158,10 @@ Note that each appContainer needs to use a different resourceId and will need it ![Package Manager demo settings](/img/docs/Package_Manager_demo_settings.png) 1. If Package Manager `start:single-app` is running, restart it. If not, start it (see the above [Installation instructions](#installation-quick-start) for the relevant command line). -### Separate http proxy server +### Separate HTTP proxy server -In some setups, the url of the http proxy server is different when accessing the sofie ui and package manager. -You can use the 'Network ID' concept in package manager to provide guidance on which to use when. +In some setups, the URL of the HTTP proxy server is different when accessing the Sofie UI and Package Manager. +You can use the 'Network ID' concept in Package Manager to provide guidance on which to use when. By adding `--networkIds=pm-net` (a semi colon separated list) when launching the exes on the CasparCG machine, the application will know to prefer certain accessors with matching values. From b76691a5106c0eeb128db02f567d98baeb31a81c Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 17 Dec 2024 11:35:51 +0100 Subject: [PATCH 06/15] chore(docs): word capitalization --- .../docs/for-developers/data-model.md | 4 ++-- .../docs/for-developers/json-config-schema.md | 2 +- .../docs/for-developers/publications.md | 2 +- .../docs/user-guide/features/access-levels.md | 2 +- .../installation/installing-package-manager.md | 18 +++++++++--------- .../for-developers/data-model.md | 4 ++-- .../for-developers/json-config-schema.md | 2 +- .../for-developers/publications.md | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/documentation/docs/for-developers/data-model.md b/packages/documentation/docs/for-developers/data-model.md index 8f887c92e8..975f29747c 100644 --- a/packages/documentation/docs/for-developers/data-model.md +++ b/packages/documentation/docs/for-developers/data-model.md @@ -18,7 +18,7 @@ In every case, any layout changes and any scheduled cleanup are performed by the This category of collections is rather loosely defined, as it ends up being everything that doesn't belong somewhere else -This consists of anything that is configurable from the Sofie UI, anything needed soley for the UI and some other bits. Additionally, there are some collections which are populated by other portions of a Sofie system, such as by package manager, through an API over DDP. +This consists of anything that is configurable from the Sofie UI, anything needed soley for the UI and some other bits. Additionally, there are some collections which are populated by other portions of a Sofie system, such as by Package Manager, through an API over DDP. Currently, there is not a very clearly defined flow for modifying these documents, with the UI often making changes directly with minimal or no validation. This includes: @@ -76,7 +76,7 @@ The collections which are owned by the ingest workers are: - [Segments](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/Segments.ts) These collections model a Rundown from the NRCS in a Sofie form. Almost all of these contain documents which are largely generated by blueprints. -Some of these collections are used by package manager to initiate work, while others form a view of the Rundown for the users, and are used as part of the model for playout. +Some of these collections are used by Package Manager to initiate work, while others form a view of the Rundown for the users, and are used as part of the model for playout. ### Playout diff --git a/packages/documentation/docs/for-developers/json-config-schema.md b/packages/documentation/docs/for-developers/json-config-schema.md index b56e6e6ee7..6970a58df3 100644 --- a/packages/documentation/docs/for-developers/json-config-schema.md +++ b/packages/documentation/docs/for-developers/json-config-schema.md @@ -55,7 +55,7 @@ Names of the enum values as generated for the typescript enum, which we display Note: Only valid for blueprint configuration. -Sometimes it can be useful to reference other values. This property can be used on string fields, to let sofie generate a dropdown populated with values valid in the current context. +Sometimes it can be useful to reference other values. This property can be used on string fields, to let Sofie generate a dropdown populated with values valid in the current context. #### `mappings` diff --git a/packages/documentation/docs/for-developers/publications.md b/packages/documentation/docs/for-developers/publications.md index ed3b377532..359a64deb5 100644 --- a/packages/documentation/docs/for-developers/publications.md +++ b/packages/documentation/docs/for-developers/publications.md @@ -32,7 +32,7 @@ There has been a recent push towards using more 'custom' publications for stream To achieve this, we have an `optimisedObserver` flow which is designed to help maange to a custom publication, with a few methods to fill in to setup the reactivity and the data transformation. One such publication is the `PieceContentStatus`, prior to version 1.50, this was computed inside the UI. -A brief overview of this publication, is that it looks at each Piece in a Rundown, and reports whether the Piece is 'OK'. This check is primarily focussed on Pieces containing clips, where it will check the metadata generated by either package manager or media manager to ensure that the clip is marked as being ready for playout, and that it has the correct format and some other quality checks. +A brief overview of this publication, is that it looks at each Piece in a Rundown, and reports whether the Piece is 'OK'. This check is primarily focussed on Pieces containing clips, where it will check the metadata generated by Package Manager to ensure that the clip is marked as being ready for playout, and that it has the correct format and some other quality checks. To do this on the client meant needing to subscribe to the whole contents of a couple of MongoDB collections, as it is not easy to determine which documents will be needed until the check is being run. This caused some issues as these collections could get rather large. We also did not always have every Piece loaded in the UI, so had to defer some of the computation to the backend via polling. diff --git a/packages/documentation/docs/user-guide/features/access-levels.md b/packages/documentation/docs/user-guide/features/access-levels.md index 466f3a6325..807e5840bc 100644 --- a/packages/documentation/docs/user-guide/features/access-levels.md +++ b/packages/documentation/docs/user-guide/features/access-levels.md @@ -7,7 +7,7 @@ sidebar_position: 3 ## Permissions There are a few different access levels that users can be assigned. They are not heirarchical, you will often need to enable multiple for each user. -Any client that can access sofie always has at least view-only access to the rundowns, and system status pages. +Any client that can access Sofie always has at least view-only access to the rundowns, and system status pages. | Level | Summary | | :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/packages/documentation/docs/user-guide/installation/installing-package-manager.md b/packages/documentation/docs/user-guide/installation/installing-package-manager.md index 1f9b414d54..b5ef254ef8 100644 --- a/packages/documentation/docs/user-guide/installation/installing-package-manager.md +++ b/packages/documentation/docs/user-guide/installation/installing-package-manager.md @@ -18,14 +18,14 @@ Although Package Manager can be used to copy any kind of file to/from a wide arr :::caution -Sofie supports only one package manager running for a Studio. Attaching more at a time will result in weird behaviour due to them fighting over reporting the statuses of packages. -If you feel like you need multiple, then you likely want to run package manager in the distributed setup instead. +Sofie supports only one Package Manager running for a Studio. Attaching more at a time will result in weird behaviour due to them fighting over reporting the statuses of packages. +If you feel like you need multiple, then you likely want to run Package Manager in the distributed setup instead. ::: :::caution -The package manager worker process is primarily tested on Windows only. It does run on Linux (without support for network shares), but has not been extensively tested. +The Package Manager worker process is primarily tested on Windows only. It does run on Linux (without support for network shares), but has not been extensively tested. ::: @@ -58,7 +58,7 @@ This first run is necessary to get the Package Manager device registered with _S ## Installation In Production -Only one package manager can be running for a Sofie Studio. If you reached this point thinking of deploying multiple, you will want to follow the distributed setup. +Only one Package Manager can be running for a Sofie Studio. If you reached this point thinking of deploying multiple, you will want to follow the distributed setup. ### Simple Setup @@ -78,7 +78,7 @@ When configuring the http proxy server in Sofie, you may need to follow extra co ### Distributed Setup -For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, package manager can be partially deployed in Docker, with just the workers running on each CasparCG machine. +For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, Package Manager can be partially deployed in Docker, with just the workers running on each CasparCG machine. An example `docker-compose` of the setup is as follows: @@ -110,7 +110,7 @@ services: context: . dockerfile: sofietv/package-manager-package-manager environment: - CORE_HOST: '172.18.0.1' # the address for connecting back to sofie core from this image + CORE_HOST: '172.18.0.1' # the address for connecting back to Sofie core from this image CORE_PORT: '3000' DEVICE_ID: 'my-package-manager-id' DEVICE_TOKEN: 'some-secret' @@ -133,7 +133,7 @@ In addition to this, you will need to run the appContainer and workers on each w ./appContainer-node.exe --appContainerId=caspar01 // This is a unique id for this instance of the appContainer --workforceURL=ws://workforce-service-ip:8070 - --resourceId=caspar01 // This should also be set in the 'resource id' field of the `casparcgLocalFolder1` accessor. This is how package manager can identify which machine is which. + --resourceId=caspar01 // This should also be set in the 'resource id' field of the `casparcgLocalFolder1` accessor. This is how Package Manager can identify which machine is which. --networkIds=pm-net // This is not necessary, but can be useful for more complex setups ``` @@ -144,7 +144,7 @@ Note that each appContainer needs to use a different resourceId and will need it ## Configuration 1. Open the _Sofie Core_ Settings page ([http://localhost:3000/settings?admin=1](http://localhost:3000/settings?admin=1)), click on your Studio, and then Peripheral Devices. -1. Click the plus button (`+`) in the Parent Devices section and configure the created device to be for your Package manager. +1. Click the plus button (`+`) in the Parent Devices section and configure the created device to be for your Package Manager. 1. On the sidebar under the current Studio, select to the Package Manager section. 1. Click the plus button under the Package Containers heading, then click the edit icon (pencil) to the right of the newly-created package container. 1. Give this package container an ID of `casparcgContainer0` and a label of `CasparCG Package Container`. @@ -167,7 +167,7 @@ By adding `--networkIds=pm-net` (a semi colon separated list) when launching the Then in the Sofie UI: -1. Return to the Package manager settings under the studio +1. Return to the Package Manager settings under the studio 1. Expand the `casparcgContainer` container. 1. Edit the `casparcgHttpProxy` accessor to have a `Base URL` that is accessible from the casparcg machines. 1. Set the `Network ID` to `pm-net` (matching what was passed in the command line) diff --git a/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md b/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md index 8f887c92e8..975f29747c 100644 --- a/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md +++ b/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md @@ -18,7 +18,7 @@ In every case, any layout changes and any scheduled cleanup are performed by the This category of collections is rather loosely defined, as it ends up being everything that doesn't belong somewhere else -This consists of anything that is configurable from the Sofie UI, anything needed soley for the UI and some other bits. Additionally, there are some collections which are populated by other portions of a Sofie system, such as by package manager, through an API over DDP. +This consists of anything that is configurable from the Sofie UI, anything needed soley for the UI and some other bits. Additionally, there are some collections which are populated by other portions of a Sofie system, such as by Package Manager, through an API over DDP. Currently, there is not a very clearly defined flow for modifying these documents, with the UI often making changes directly with minimal or no validation. This includes: @@ -76,7 +76,7 @@ The collections which are owned by the ingest workers are: - [Segments](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/Segments.ts) These collections model a Rundown from the NRCS in a Sofie form. Almost all of these contain documents which are largely generated by blueprints. -Some of these collections are used by package manager to initiate work, while others form a view of the Rundown for the users, and are used as part of the model for playout. +Some of these collections are used by Package Manager to initiate work, while others form a view of the Rundown for the users, and are used as part of the model for playout. ### Playout diff --git a/packages/documentation/versioned_docs/version-1.50.0/for-developers/json-config-schema.md b/packages/documentation/versioned_docs/version-1.50.0/for-developers/json-config-schema.md index 1d6df1db25..7a7536c1d0 100644 --- a/packages/documentation/versioned_docs/version-1.50.0/for-developers/json-config-schema.md +++ b/packages/documentation/versioned_docs/version-1.50.0/for-developers/json-config-schema.md @@ -55,7 +55,7 @@ Names of the enum values as generated for the typescript enum, which we display Note: Only valid for blueprint configuration. -Sometimes it can be useful to reference other values. This property can be used on string fields, to let sofie generate a dropdown populated with values valid in the current context. +Sometimes it can be useful to reference other values. This property can be used on string fields, to let Sofie generate a dropdown populated with values valid in the current context. #### `mappings` diff --git a/packages/documentation/versioned_docs/version-1.50.0/for-developers/publications.md b/packages/documentation/versioned_docs/version-1.50.0/for-developers/publications.md index ed3b377532..d584a5013c 100644 --- a/packages/documentation/versioned_docs/version-1.50.0/for-developers/publications.md +++ b/packages/documentation/versioned_docs/version-1.50.0/for-developers/publications.md @@ -32,7 +32,7 @@ There has been a recent push towards using more 'custom' publications for stream To achieve this, we have an `optimisedObserver` flow which is designed to help maange to a custom publication, with a few methods to fill in to setup the reactivity and the data transformation. One such publication is the `PieceContentStatus`, prior to version 1.50, this was computed inside the UI. -A brief overview of this publication, is that it looks at each Piece in a Rundown, and reports whether the Piece is 'OK'. This check is primarily focussed on Pieces containing clips, where it will check the metadata generated by either package manager or media manager to ensure that the clip is marked as being ready for playout, and that it has the correct format and some other quality checks. +A brief overview of this publication, is that it looks at each Piece in a Rundown, and reports whether the Piece is 'OK'. This check is primarily focussed on Pieces containing clips, where it will check the metadata generated by either Package Manager or media manager to ensure that the clip is marked as being ready for playout, and that it has the correct format and some other quality checks. To do this on the client meant needing to subscribe to the whole contents of a couple of MongoDB collections, as it is not easy to determine which documents will be needed until the check is being run. This caused some issues as these collections could get rather large. We also did not always have every Piece loaded in the UI, so had to defer some of the computation to the backend via polling. From 7f1bc0245eea5a59b927c750a6d55c081030c874 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Dec 2024 15:48:12 +0000 Subject: [PATCH 07/15] chore: fix typo --- meteor/server/lib/rest/v1/studios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/server/lib/rest/v1/studios.ts b/meteor/server/lib/rest/v1/studios.ts index 3e5827c868..10c9299407 100644 --- a/meteor/server/lib/rest/v1/studios.ts +++ b/meteor/server/lib/rest/v1/studios.ts @@ -220,5 +220,5 @@ export interface APIStudioSettings { allowHold?: boolean allowPieceDirectPlay?: boolean enableBuckets?: boolean - enableEvaluationForm: true + enableEvaluationForm?: boolean } From 8704ff819e895d8d60a5a24628c286677eb07735 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 12:45:51 +0000 Subject: [PATCH 08/15] fix: sofie logo not showing in dev --- meteor/server/lib.ts | 6 +++++- packages/webui/vite.config.mts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/meteor/server/lib.ts b/meteor/server/lib.ts index a58d10c534..1c944e58a2 100644 --- a/meteor/server/lib.ts +++ b/meteor/server/lib.ts @@ -4,6 +4,7 @@ import fs from 'fs' import path from 'path' import { logger } from './logging' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { Meteor } from 'meteor/meteor' /** Returns absolute path to programs/server directory of your compiled application, without trailing slash. */ export function getAbsolutePath(): string { @@ -27,7 +28,10 @@ export function extractFunctionSignature(f: Function): string[] | undefined { export type Translations = Record // The /public directory in a Meteor app -export const public_dir = path.join(process.cwd(), '../web.browser/app') +export const public_dir = Meteor.isProduction + ? path.join(process.cwd(), '../web.browser/app') + : // In development, find the webui package and use its public directory + path.join(process.cwd(), '../../../../../../packages/webui/public') /** * Get the i18next locale object for a given `languageCode`. If the translations file can not be found or it can't be diff --git a/packages/webui/vite.config.mts b/packages/webui/vite.config.mts index 8ca2ac96c3..fff95dd146 100644 --- a/packages/webui/vite.config.mts +++ b/packages/webui/vite.config.mts @@ -59,6 +59,7 @@ export default defineConfig({ '/api': 'http://127.0.0.1:3000', '/site.webmanifest': 'http://127.0.0.1:3000', '/meteor-runtime-config.js': 'http://127.0.0.1:3000', + '/images/sofie-logo.svg': 'http://127.0.0.1:3000', '/websocket': { target: `ws://127.0.0.1:3000`, ws: true, From c810d263f6e3c3579c068a47d0ee0051940dbfae Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 13:28:56 +0000 Subject: [PATCH 09/15] chore: convert some class react components to functional --- .../lib/ConnectionStatusNotification.tsx | 38 +--- .../notifications/NotificationCenterPanel.tsx | 6 - .../src/client/ui/ClockView/Timediff.tsx | 31 ++- .../src/client/ui/PieceIcons/PieceIcon.tsx | 12 +- .../ui/PieceIcons/Renderers/CamInputIcon.tsx | 72 +++---- .../Renderers/GraphicsInputIcon.tsx | 55 +++-- .../Renderers/LiveSpeakInputIcon.tsx | 66 +++--- .../PieceIcons/Renderers/RemoteInputIcon.tsx | 12 +- .../PieceIcons/Renderers/UnknownInputIcon.tsx | 58 +++-- .../ui/PieceIcons/Renderers/VTInputIcon.tsx | 56 +++-- packages/webui/src/client/ui/RundownView.tsx | 132 ++++++------ .../ui/RundownView/PlaylistLoopingHeader.tsx | 12 +- .../ui/RundownView/RundownDividerHeader.tsx | 77 +++---- .../RundownTiming/PlaylistEndTiming.tsx | 198 ++++++++---------- .../RundownTiming/PlaylistStartTiming.tsx | 121 ++++++----- .../RundownView/RundownTiming/RundownName.tsx | 154 +++++++------- .../RundownView/RundownTiming/TimeOfDay.tsx | 18 +- .../ui/SegmentTimeline/BreakSegment.tsx | 54 ++--- .../src/client/ui/Shelf/EndWordsPanel.tsx | 89 ++++---- .../ItemRenderers/ItemRendererFactory.ts | 2 +- .../ItemRenderers/NoraItemRenderer.tsx | 94 ++++----- .../client/ui/Shelf/NextBreakTimingPanel.tsx | 42 ---- .../src/client/ui/Shelf/NextInfoPanel.tsx | 101 +++++---- .../src/client/ui/Shelf/PartNamePanel.tsx | 114 ++++------ .../src/client/ui/Shelf/PartTimingPanel.tsx | 107 +++++----- .../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 1 - .../src/client/ui/Shelf/PlaylistNamePanel.tsx | 82 +++----- .../src/client/ui/Shelf/SegmentNamePanel.tsx | 62 ++---- .../client/ui/Shelf/SegmentTimingPanel.tsx | 77 ++++--- .../webui/src/client/ui/Status/DebugState.tsx | 109 +++++----- 30 files changed, 890 insertions(+), 1162 deletions(-) delete mode 100644 packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx diff --git a/packages/webui/src/client/lib/ConnectionStatusNotification.tsx b/packages/webui/src/client/lib/ConnectionStatusNotification.tsx index fcb7e66e50..f7995bb567 100644 --- a/packages/webui/src/client/lib/ConnectionStatusNotification.tsx +++ b/packages/webui/src/client/lib/ConnectionStatusNotification.tsx @@ -2,10 +2,7 @@ import { Meteor } from 'meteor/meteor' import { DDP } from 'meteor/ddp' import * as React from 'react' import * as _ from 'underscore' - -import { Translated } from './ReactMeteorData/react-meteor-data' import { MomentFromNow } from './Moment' - import { NotificationCenter, NoticeLevel, @@ -14,13 +11,14 @@ import { NotifierHandle, } from './notifications/notifications' import { WithManagedTracker } from './reactiveData/reactiveDataHelper' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { NotificationCenterPopUps } from './notifications/NotificationCenterPanel' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { ICoreSystem, ServiceMessage, Criticality } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { TFunction } from 'react-i18next' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { CoreSystem } from '../collections' +import { useEffect } from 'react' export class ConnectionStatusNotifier extends WithManagedTracker { private _notificationList: NotificationList @@ -233,30 +231,16 @@ function createSystemNotification(cs: ICoreSystem | undefined): Notification | u return undefined } -interface IProps {} -interface IState { - dismissed: boolean -} - -export const ConnectionStatusNotification = withTranslation()( - class ConnectionStatusNotification extends React.Component, IState> { - private notifier: ConnectionStatusNotifier | undefined - - constructor(props: Translated) { - super(props) - } +export function ConnectionStatusNotification(): JSX.Element { + const { t } = useTranslation() - componentDidMount(): void { - this.notifier = new ConnectionStatusNotifier(this.props.t) - } + useEffect(() => { + const notifier = new ConnectionStatusNotifier(t) - componentWillUnmount(): void { - if (this.notifier) this.notifier.stop() + return () => { + notifier.stop() } + }, [t]) - render(): JSX.Element { - // this.props.connected - return - } - } -) + return +} diff --git a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx index 4938b40edc..339f689172 100644 --- a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx +++ b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx @@ -455,9 +455,6 @@ export const NotificationCenterPopUps = translateWithTracker (
@@ -486,9 +483,6 @@ interface IToggleProps { /** * A button for with a count of notifications in the Notification Center - * @export - * @class NotificationCenterPanelToggle - * @extends React.Component */ export function NotificationCenterPanelToggle({ className, diff --git a/packages/webui/src/client/ui/ClockView/Timediff.tsx b/packages/webui/src/client/ui/ClockView/Timediff.tsx index 51f96308a5..55af2adc0b 100644 --- a/packages/webui/src/client/ui/ClockView/Timediff.tsx +++ b/packages/webui/src/client/ui/ClockView/Timediff.tsx @@ -1,22 +1,19 @@ -import * as React from 'react' import ClassNames from 'classnames' import { RundownUtils } from '../../lib/rundown' -export const Timediff = class Timediff extends React.Component<{ time: number }> { - render(): JSX.Element { - const time = -this.props.time - const isNegative = Math.floor(time / 1000) > 0 - const timeString = RundownUtils.formatDiffToTimecode(time, true, false, true, false, true, '', false, true) +export function Timediff({ time: rawTime }: { time: number }): JSX.Element { + const time = -rawTime + const isNegative = Math.floor(time / 1000) > 0 + const timeString = RundownUtils.formatDiffToTimecode(time, true, false, true, false, true, '', false, true) - return ( - -30, - })} - > - {timeString} - - ) - } + return ( + -30, + })} + > + {timeString} + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx b/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx index 1285e5695f..c9f3374159 100644 --- a/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx @@ -6,13 +6,13 @@ import { RemoteContent, EvsContent, } from '@sofie-automation/blueprints-integration' -import CamInputIcon from './Renderers/CamInputIcon' -import VTInputIcon from './Renderers/VTInputIcon' +import { CamInputIcon } from './Renderers/CamInputIcon' +import { VTInputIcon } from './Renderers/VTInputIcon' import SplitInputIcon from './Renderers/SplitInputIcon' -import RemoteInputIcon from './Renderers/RemoteInputIcon' -import LiveSpeakInputIcon from './Renderers/LiveSpeakInputIcon' -import GraphicsInputIcon from './Renderers/GraphicsInputIcon' -import UnknownInputIcon from './Renderers/UnknownInputIcon' +import { RemoteInputIcon } from './Renderers/RemoteInputIcon' +import { LiveSpeakInputIcon } from './Renderers/LiveSpeakInputIcon' +import { GraphicsInputIcon } from './Renderers/GraphicsInputIcon' +import { UnknownInputIcon } from './Renderers/UnknownInputIcon' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { findPieceInstanceToShow, findPieceInstanceToShowFromInstances } from './utils' diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx index 305420d58f..88e0126e04 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx @@ -1,41 +1,43 @@ -import * as React from 'react' - // @todo: use dynamic data for camera number -export default class CamInputIcon extends React.Component<{ inputIndex?: string; abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - {this.props.abbreviation ? this.props.abbreviation : 'C'} - - {this.props.inputIndex !== undefined ? this.props.inputIndex : ''} - + {abbreviation ? abbreviation : 'C'} + + {inputIndex !== undefined ? inputIndex : ''} - - - ) - } + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx index 582ada7e26..969db38be2 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx @@ -1,33 +1,30 @@ -import * as React from 'react' -export default class GraphicsInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - {this.props.abbreviation ? this.props.abbreviation : 'G'} - - - - ) - } + {abbreviation ? abbreviation : 'G'} + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx index f7fa3635ae..e623d132e1 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx @@ -1,39 +1,35 @@ -import * as React from 'react' - -export default class LiveSpeakInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - - - - - + + + + + + + - - {this.props.abbreviation ? this.props.abbreviation : 'LSK'} - - - - ) - } + {abbreviation ? abbreviation : 'LSK'} + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx index 6d7058618c..13cdbff73a 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx @@ -35,11 +35,17 @@ export function BaseRemoteInputIcon(props: Readonly): JSX.Element { +export function RemoteInputIcon({ + inputIndex, + abbreviation, +}: { + inputIndex?: string + abbreviation?: string +}): JSX.Element { return ( - {props.abbreviation ? props.abbreviation : 'LIVE'} - {props.inputIndex ?? ''} + {abbreviation ? abbreviation : 'LIVE'} + {inputIndex ?? ''} ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx index 0d64a1d642..e78367d200 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx @@ -1,34 +1,30 @@ -import * as React from 'react' - -export default class UnknownInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - ? - - - - ) - } + ? + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx index 4d701d6e2a..d44ffe0311 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx @@ -1,34 +1,30 @@ -import * as React from 'react' - -export default class VTInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - {this.props.abbreviation ? this.props.abbreviation : 'VT'} - - - - ) - } + {abbreviation ? abbreviation : 'VT'} + + + + ) } diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 9210c801db..a95fcefdb1 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -12,7 +12,7 @@ import { useTracker, } from '../lib/ReactMeteorData/react-meteor-data' import { VTContent, TSR, NoteSeverity, ISourceLayer } from '@sofie-automation/blueprints-integration' -import { withTranslation, WithTranslation } from 'react-i18next' +import { useTranslation, withTranslation } from 'react-i18next' import timer from 'react-timer-hoc' import * as CoreIcon from '@nrk/core-icons/jsx' import { Spinner } from '../lib/Spinner' @@ -279,75 +279,69 @@ interface ITimingDisplayProps { layout: RundownLayoutRundownHeader | undefined } -const TimingDisplay = withTranslation()( - withTiming()( - class TimingDisplay extends React.Component>> { - render(): JSX.Element | null { - const { t, rundownPlaylist, currentRundown } = this.props - - if (!rundownPlaylist) return null - - const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) - const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) - const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) - const showEndTiming = - !this.props.timingDurations.rundownsBeforeNextBreak || - !this.props.layout?.showNextBreakTiming || - (this.props.timingDurations.rundownsBeforeNextBreak.length > 0 && - (!this.props.layout?.hideExpectedEndBeforeBreak || - (this.props.timingDurations.breakIsLastRundown && this.props.layout?.lastRundownIsNotBreak))) - const showNextBreakTiming = - rundownPlaylist.startedPlayback && - this.props.timingDurations.rundownsBeforeNextBreak?.length && - this.props.layout?.showNextBreakTiming && - !(this.props.timingDurations.breakIsLastRundown && this.props.layout.lastRundownIsNotBreak) - - return ( -
- - - - {rundownPlaylist.currentPartInfo && ( - - - - {rundownPlaylist.holdState && rundownPlaylist.holdState !== RundownHoldState.COMPLETE ? ( -
{t('Hold')}
- ) : null} -
- )} - {showEndTiming ? ( - - ) : null} - {showNextBreakTiming ? ( - - ) : null} -
- ) - } - } +const TimingDisplay = withTiming()(function TimingDisplay({ + rundownPlaylist, + currentRundown, + rundownCount, + layout, + timingDurations, +}: WithTiming): JSX.Element | null { + const { t } = useTranslation() + + if (!rundownPlaylist) return null + + const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) + const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) + const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) + const showEndTiming = + !timingDurations.rundownsBeforeNextBreak || + !layout?.showNextBreakTiming || + (timingDurations.rundownsBeforeNextBreak.length > 0 && + (!layout?.hideExpectedEndBeforeBreak || (timingDurations.breakIsLastRundown && layout?.lastRundownIsNotBreak))) + const showNextBreakTiming = + rundownPlaylist.startedPlayback && + timingDurations.rundownsBeforeNextBreak?.length && + layout?.showNextBreakTiming && + !(timingDurations.breakIsLastRundown && layout.lastRundownIsNotBreak) + + return ( +
+ + + + {rundownPlaylist.currentPartInfo && ( + + + + {rundownPlaylist.holdState && rundownPlaylist.holdState !== RundownHoldState.COMPLETE ? ( +
{t('Hold')}
+ ) : null} +
+ )} + {showEndTiming ? ( + + ) : null} + {showNextBreakTiming ? ( + + ) : null} +
) -) +}) interface IRundownHeaderProps { playlist: DBRundownPlaylist diff --git a/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx b/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx index 97ad4e434f..673844e49a 100644 --- a/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx @@ -1,7 +1,6 @@ import React from 'react' import classNames from 'classnames' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' import { LoopingIcon } from '../../lib/ui/icons/looping' import { WithTiming, withTiming } from './RundownTiming/withTiming' @@ -44,10 +43,9 @@ interface ILoopingHeaderProps { multiRundown?: boolean showCountdowns?: boolean } -export const PlaylistLoopingHeader = withTranslation()(function PlaylistLoopingHeader( - props: Translated -) { - const { t, position, multiRundown, showCountdowns } = props +export function PlaylistLoopingHeader({ position, multiRundown, showCountdowns }: ILoopingHeaderProps): JSX.Element { + const { t } = useTranslation() + return (
) -}) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx index 3c372db734..fb2a11951f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx @@ -1,9 +1,8 @@ import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import Moment from 'react-moment' import { TimingDataResolution, TimingTickResolution, withTiming, WithTiming } from './RundownTiming/withTiming' import { RundownUtils } from '../../lib/rundown' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' @@ -14,50 +13,41 @@ interface IProps { const QUATER_DAY = 6 * 60 * 60 * 1000 +interface MarkerCountdownProps { + markerTimestamp: number | undefined + className?: string | undefined +} + /** * This is a countdown to the rundown's Expected Start or Expected End time. It shows nothing if the expectedStart is undefined * or the time to Expected Start/End from now is larger than 6 hours. */ -const MarkerCountdownText = withTranslation()( - withTiming< - Translated<{ - markerTimestamp: number | undefined - className?: string | undefined - }>, - {} - >({ - filter: 'currentTime', - tickResolution: TimingTickResolution.Low, - dataResolution: TimingDataResolution.Synced, - })(function MarkerCountdown( - props: Translated< - WithTiming<{ - markerTimestamp: number | undefined - className?: string | undefined - }> - > - ) { - const { t } = props - if (props.markerTimestamp === undefined) return null +const MarkerCountdownText = withTiming({ + filter: 'currentTime', + tickResolution: TimingTickResolution.Low, + dataResolution: TimingDataResolution.Synced, +})(function MarkerCountdown(props: WithTiming) { + const { t } = useTranslation() - const time = props.markerTimestamp - (props.timingDurations.currentTime || 0) + if (props.markerTimestamp === undefined) return null - if (time < QUATER_DAY) { - return ( - - {time > 0 - ? t('(in: {{time}})', { - time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), - }) - : t('({{time}} ago)', { - time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), - })} - - ) - } - return null - }) -) + const time = props.markerTimestamp - (props.timingDurations.currentTime || 0) + + if (time < QUATER_DAY) { + return ( + + {time > 0 + ? t('(in: {{time}})', { + time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), + }) + : t('({{time}} ago)', { + time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), + })} + + ) + } + return null +}) /** * This is a component for showing the title of the rundown, it's expectedStart and expectedDuration and @@ -67,8 +57,9 @@ const MarkerCountdownText = withTranslation()( * * The component should be minimally reactive. */ -export const RundownDividerHeader = withTranslation()(function RundownDividerHeader(props: Translated) { - const { t, rundown, playlist } = props +export function RundownDividerHeader({ rundown, playlist }: IProps): JSX.Element { + const { t } = useTranslation() + const expectedStart = PlaylistTiming.getExpectedStart(rundown.timing) const expectedDuration = PlaylistTiming.getExpectedDuration(rundown.timing) const expectedEnd = PlaylistTiming.getExpectedEnd(rundown.timing) @@ -120,4 +111,4 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea ) : null}
) -}) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx index d48b6b22b8..fc7d0f1790 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx @@ -1,8 +1,7 @@ import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' import { getCurrentTime } from '../../../lib/systemTime' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../../lib/rundown' import { withTiming, WithTiming } from './withTiming' import ClassNames from 'classnames' @@ -22,116 +21,95 @@ interface IEndTimingProps { hidePlannedEnd?: boolean hideCountdown?: boolean hideDiff?: boolean - rundownCount: number } -export const PlaylistEndTiming = withTranslation()( - withTiming()( - class PlaylistEndTiming extends React.Component>> { - render(): JSX.Element { - const { t } = this.props - const { rundownPlaylist, expectedStart, expectedEnd, expectedDuration } = this.props +export const PlaylistEndTiming = withTiming()(function PlaylistEndTiming({ + rundownPlaylist, + loop, + expectedStart, + expectedDuration, + expectedEnd, + endLabel, + hidePlannedEndLabel, + hideDiffLabel, + hidePlannedEnd, + hideCountdown, + hideDiff, + timingDurations, +}: WithTiming): JSX.Element { + const { t } = useTranslation() - const overUnderClock = getPlaylistTimingDiff(rundownPlaylist, this.props.timingDurations) ?? 0 - const now = this.props.timingDurations.currentTime ?? getCurrentTime() + const overUnderClock = getPlaylistTimingDiff(rundownPlaylist, timingDurations) ?? 0 + const now = timingDurations.currentTime ?? getCurrentTime() - return ( - - {!this.props.hidePlannedEnd ? ( - this.props.expectedEnd ? ( - !rundownPlaylist.startedPlayback ? ( - - {!this.props.hidePlannedEndLabel && ( - {this.props.endLabel ?? t('Planned End')} - )} - - - ) : ( - - {!this.props.hidePlannedEndLabel && ( - {this.props.endLabel ?? t('Expected End')} - )} - - - ) - ) : this.props.timingDurations ? ( - isLoopRunning(this.props.rundownPlaylist) ? ( - this.props.timingDurations.partCountdown && - rundownPlaylist.activationId && - rundownPlaylist.currentPartInfo ? ( - - {!this.props.hidePlannedEndLabel && ( - {t('Next Loop at')} - )} - - - ) : null - ) : ( - - {!this.props.hidePlannedEndLabel && ( - {this.props.endLabel ?? t('Expected End')} - )} - - - ) - ) : null - ) : null} - {!this.props.loop && - !this.props.hideCountdown && - (expectedEnd ? ( - - {RundownUtils.formatDiffToTimecode(now - expectedEnd, true, true, true)} - - ) : expectedStart && expectedDuration ? ( - - {RundownUtils.formatDiffToTimecode( - getCurrentTime() - (expectedStart + expectedDuration), - true, - true, - true - )} - - ) : null)} - {!this.props.hideDiff ? ( - this.props.timingDurations ? ( - = 0, - })} - role="timer" - > - {!this.props.hideDiffLabel && {t('Diff')}} - {RundownUtils.formatDiffToTimecode( - overUnderClock, - true, - false, - true, - true, - true, - undefined, - true, - true - )} - - ) : null - ) : null} - - ) - } - } + return ( + + {!hidePlannedEnd ? ( + expectedEnd ? ( + !rundownPlaylist.startedPlayback ? ( + + {!hidePlannedEndLabel && {endLabel ?? t('Planned End')}} + + + ) : ( + + {!hidePlannedEndLabel && ( + {endLabel ?? t('Expected End')} + )} + + + ) + ) : timingDurations ? ( + isLoopRunning(rundownPlaylist) ? ( + timingDurations.partCountdown && rundownPlaylist.activationId && rundownPlaylist.currentPartInfo ? ( + + {!hidePlannedEndLabel && {t('Next Loop at')}} + + + ) : null + ) : ( + + {!hidePlannedEndLabel && ( + {endLabel ?? t('Expected End')} + )} + + + ) + ) : null + ) : null} + {!loop && + !hideCountdown && + (expectedEnd ? ( + + {RundownUtils.formatDiffToTimecode(now - expectedEnd, true, true, true)} + + ) : expectedStart && expectedDuration ? ( + + {RundownUtils.formatDiffToTimecode(getCurrentTime() - (expectedStart + expectedDuration), true, true, true)} + + ) : null)} + {!hideDiff ? ( + timingDurations ? ( + = 0, + })} + role="timer" + > + {!hideDiffLabel && {t('Diff')}} + {RundownUtils.formatDiffToTimecode(overUnderClock, true, false, true, true, true, undefined, true, true)} + + ) : null + ) : null} + ) -) +}) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx index 9acf15cbd2..aca0e98cff 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx @@ -1,7 +1,6 @@ import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { withTiming, WithTiming } from './withTiming' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownUtils } from '../../../lib/rundown' @@ -9,70 +8,70 @@ import { getCurrentTime } from '../../../lib/systemTime' import ClassNames from 'classnames' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -interface IEndTimingProps { +interface IStartTimingProps { rundownPlaylist: DBRundownPlaylist hidePlannedStart?: boolean hideDiff?: boolean plannedStartText?: string } -export const PlaylistStartTiming = withTranslation()( - withTiming()( - class PlaylistStartTiming extends React.Component>> { - render(): JSX.Element { - const { t, rundownPlaylist } = this.props - const playlistExpectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) - const playlistExpectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) - const playlistExpectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) - const expectedStart = playlistExpectedStart - ? playlistExpectedStart - : playlistExpectedDuration && playlistExpectedEnd - ? playlistExpectedEnd - playlistExpectedDuration - : undefined +export const PlaylistStartTiming = withTiming()(function PlaylistStartTiming({ + rundownPlaylist, + hidePlannedStart, + hideDiff, + plannedStartText, +}: WithTiming): JSX.Element { + const { t } = useTranslation() - return ( - - {!this.props.hidePlannedStart && - (rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? ( - - {t('Started')} - - - ) : playlistExpectedStart ? ( - - {this.props.plannedStartText || t('Planned Start')} - - - ) : playlistExpectedEnd && playlistExpectedDuration ? ( - - {this.props.plannedStartText || t('Expected Start')} - - - ) : null)} - {!this.props.hideDiff && expectedStart && ( - expectedStart, - light: getCurrentTime() <= expectedStart, - })} - role="timer" - > - {t('Diff')} - {rundownPlaylist.startedPlayback - ? RundownUtils.formatDiffToTimecode( - rundownPlaylist.startedPlayback - expectedStart, - true, - false, - true, - true, - true - ) - : RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} - - )} - - ) - } - } + const playlistExpectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) + const playlistExpectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) + const playlistExpectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) + const expectedStart = playlistExpectedStart + ? playlistExpectedStart + : playlistExpectedDuration && playlistExpectedEnd + ? playlistExpectedEnd - playlistExpectedDuration + : undefined + + return ( + + {!hidePlannedStart && + (rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? ( + + {t('Started')} + + + ) : playlistExpectedStart ? ( + + {plannedStartText || t('Planned Start')} + + + ) : playlistExpectedEnd && playlistExpectedDuration ? ( + + {plannedStartText || t('Expected Start')} + + + ) : null)} + {!hideDiff && expectedStart && ( + expectedStart, + light: getCurrentTime() <= expectedStart, + })} + role="timer" + > + {t('Diff')} + {rundownPlaylist.startedPlayback + ? RundownUtils.formatDiffToTimecode( + rundownPlaylist.startedPlayback - expectedStart, + true, + false, + true, + true, + true + ) + : RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} + + )} + ) -) +}) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx index 08dc97104d..ffcb578b9d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx @@ -1,6 +1,4 @@ -import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { useTranslation } from 'react-i18next' import { withTiming, WithTiming } from './withTiming' import ClassNames from 'classnames' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -18,80 +16,78 @@ interface IRundownNameProps { hideDiff?: boolean } -export const RundownName = withTranslation()( - withTiming()( - class RundownName extends React.Component>> { - render(): JSX.Element { - const { rundownPlaylist, currentRundown, rundownCount, t } = this.props - const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) - const isPlaylistLooping = isLoopDefined(rundownPlaylist) - return ( -
expectedStart, - })} - > - {currentRundown && (rundownPlaylist.name !== currentRundown.name || rundownCount > 1) ? ( -

- {isPlaylistLooping && } {currentRundown.name} {rundownPlaylist.name} -

- ) : ( -

- {isPlaylistLooping && } {rundownPlaylist.name} -

- )} - {!this.props.hideDiff && - rundownPlaylist.startedPlayback && - rundownPlaylist.activationId && - !rundownPlaylist.rehearsal - ? expectedStart && - RundownUtils.formatDiffToTimecode( - rundownPlaylist.startedPlayback - expectedStart, - true, - false, - true, - true, - true - ) - : expectedStart && - RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} -
- ) - } - } +export const RundownName = withTiming()(function RundownName({ + rundownPlaylist, + currentRundown, + rundownCount, + hideDiff, +}: WithTiming): JSX.Element { + const { t } = useTranslation() + + const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) + const isPlaylistLooping = isLoopDefined(rundownPlaylist) + + return ( +
expectedStart, + })} + > + {currentRundown && (rundownPlaylist.name !== currentRundown.name || rundownCount > 1) ? ( +

+ {isPlaylistLooping && } {currentRundown.name} {rundownPlaylist.name} +

+ ) : ( +

+ {isPlaylistLooping && } {rundownPlaylist.name} +

+ )} + {!hideDiff && rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal + ? expectedStart && + RundownUtils.formatDiffToTimecode( + rundownPlaylist.startedPlayback - expectedStart, + true, + false, + true, + true, + true + ) + : expectedStart && + RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} +
) -) +}) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx index bbbdd0ed2f..c571410ce7 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx @@ -1,14 +1,10 @@ -import { WithTranslation, withTranslation } from 'react-i18next' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { withTiming, WithTiming } from './withTiming' import Moment from 'react-moment' -export const TimeOfDay = withTranslation()( - withTiming()(function RundownName(props: Translated>) { - return ( - - - - ) - }) -) +export const TimeOfDay = withTiming<{}, {}>()(function TimeOfDay({ timingDurations }: WithTiming<{}>) { + return ( + + + + ) +}) diff --git a/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx b/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx index 62adc8a71e..ebb47cb071 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx @@ -1,7 +1,5 @@ -import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../lib/rundown' import { WithTiming, withTiming } from '../RundownView/RundownTiming/withTiming' @@ -9,37 +7,29 @@ interface IProps { breakTime: number | undefined } -class BreakSegmentInner extends React.Component>> { - constructor(props: Translated>) { - super(props) - } +function BreakSegmentInner({ breakTime, timingDurations }: WithTiming): JSX.Element { + const { t } = useTranslation() - render(): JSX.Element { - const { t } = this.props - const displayTimecode = - this.props.breakTime && this.props.timingDurations.currentTime - ? this.props.breakTime - this.props.timingDurations.currentTime - : undefined + const displayTimecode = breakTime && timingDurations.currentTime ? breakTime - timingDurations.currentTime : undefined - return ( -
-
-

- {this.props.breakTime && }  - {t('BREAK')} -

-
- {displayTimecode && ( -
- {t('Break In')} - - {RundownUtils.formatDiffToTimecode(displayTimecode, false, undefined, undefined, undefined, true)} - -
- )} + return ( +
+
+

+ {breakTime && }  + {t('BREAK')} +

- ) - } + {displayTimecode && ( +
+ {t('Break In')} + + {RundownUtils.formatDiffToTimecode(displayTimecode, false, undefined, undefined, undefined, true)} + +
+ )} +
+ ) } -export const BreakSegment = withTranslation()(withTiming()(BreakSegmentInner)) +export const BreakSegment = withTiming()(BreakSegmentInner) diff --git a/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx b/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx index 6c5a695856..662793b780 100644 --- a/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import * as _ from 'underscore' import ClassNames from 'classnames' import { @@ -8,7 +7,7 @@ import { } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' import { dashboardElementStyle } from './DashboardPanel' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ScriptContent } from '@sofie-automation/blueprints-integration' @@ -17,65 +16,55 @@ import { getScriptPreview } from '../../lib/ui/scriptPreview' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { PieceInstances } from '../../collections' import { ReadonlyDeep } from 'type-fest' +import { useTranslation } from 'react-i18next' interface IEndsWordsPanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutEndWords playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } -interface IEndsWordsPanelTrackedProps { - livePieceInstance?: PieceInstance -} - -interface IState {} - -class EndWordsPanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) +export function EndWordsPanel({ layout, panel, playlist, showStyleBase }: IEndsWordsPanelProps): JSX.Element { + const { t } = useTranslation() - const { t, livePieceInstance, panel } = this.props - const content = livePieceInstance?.piece.content as Partial | undefined + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) - const { endOfScript } = getScriptPreview(content?.fullScript || '') + const livePieceInstance = useTracker( + () => getPieceWithScript(playlist, showStyleBase, panel), + [playlist, showStyleBase, panel] + ) - return ( -
-
- {!this.props.panel.hideLabel && {t('End Words')}} - ‎{endOfScript}‎ -
+ const content = livePieceInstance?.piece.content as Partial | undefined + + const { endOfScript } = getScriptPreview(content?.fullScript || '') + + return ( +
+
+ {!panel.hideLabel && {t('End Words')}} + ‎{endOfScript}‎
- ) - } +
+ ) } -export const EndWordsPanel = translateWithTracker( - (props: IEndsWordsPanelProps) => { - return { livePieceInstance: getPieceWithScript(props) } - }, - (_data, props: IEndsWordsPanelProps, nextProps: IEndsWordsPanelProps) => { - return !_.isEqual(props, nextProps) - } -)(EndWordsPanelInner) - -function getPieceWithScript(props: IEndsWordsPanelProps): PieceInstance | undefined { - const currentPartInstanceId = props.playlist.currentPartInfo?.partInstanceId +function getPieceWithScript( + playlist: DBRundownPlaylist, + showStyleBase: UIShowStyleBase, + panel: RundownLayoutEndWords +): PieceInstance | undefined { + const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId const unfinishedPiecesIncludingFinishedPiecesWhereEndTimeHaveNotBeenSet = getUnfinishedPieceInstancesReactive( - props.playlist, - props.showStyleBase + playlist, + showStyleBase ) const highestStartedPlayback = unfinishedPiecesIncludingFinishedPiecesWhereEndTimeHaveNotBeenSet.reduce( @@ -90,7 +79,7 @@ function getPieceWithScript(props: IEndsWordsPanelProps): PieceInstance | undefi ) const activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId) - const hasAdditionalLayer: boolean = props.panel.additionalLayers?.some((s) => activeLayers.includes(s)) ?? false + const hasAdditionalLayer: boolean = panel.additionalLayers?.some((s) => activeLayers.includes(s)) ?? false if (!hasAdditionalLayer) { return undefined @@ -100,15 +89,15 @@ function getPieceWithScript(props: IEndsWordsPanelProps): PieceInstance | undefi const piecesInPart: PieceInstance[] = currentPartInstanceId ? PieceInstances.find({ partInstanceId: currentPartInstanceId, - playlistActivationId: props.playlist.activationId, + playlistActivationId: playlist.activationId, }).fetch() : [] - return props.panel.requiredLayerIds && props.panel.requiredLayerIds.length + return panel.requiredLayerIds && panel.requiredLayerIds.length ? piecesInPart.find((piece: PieceInstance) => { return ( - (props.panel.requiredLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 && - piece.partInstanceId === props.playlist.currentPartInfo?.partInstanceId + (panel.requiredLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 && + piece.partInstanceId === playlist.currentPartInfo?.partInstanceId ) }) : undefined diff --git a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts index 88755ddf81..94a77dc8de 100644 --- a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts +++ b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts @@ -1,6 +1,6 @@ import * as React from 'react' import DefaultItemRenderer from './DefaultItemRenderer' -import NoraItemRenderer, { isNoraItem } from './NoraItemRenderer' +import { NoraItemRenderer, isNoraItem } from './NoraItemRenderer' import ActionItemRenderer, { isActionItem } from './ActionItemRenderer' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' diff --git a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx index f24dafd760..9c77f983e3 100644 --- a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx +++ b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx @@ -1,15 +1,15 @@ -import * as React from 'react' import { NoraContent } from '@sofie-automation/blueprints-integration' import { IModalAttributes, Modal } from '../../../../lib/ui/containers/modals/Modal' import { NoraItemEditor } from './NoraItemEditor' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' import { RundownUtils } from '../../../../lib/rundown' -import { withTranslation, WithTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import InspectorTitle from './InspectorTitle' import { ErrorBoundary } from '../../../../lib/ErrorBoundary' import { IAdLibListItem } from '../../AdLibListItem' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' +import { useState } from 'react' export { isNoraItem } @@ -19,66 +19,44 @@ interface INoraSuperRendererProps { studio: UIStudio } -interface INoraSuperRendererState { - editMode: boolean -} - -export default withTranslation()( - class NoraItemRenderer extends React.Component { - constructor(props: INoraSuperRendererProps & WithTranslation) { - super(props) - - this.state = { - editMode: false, - } - } - - setEditMode(enabled: boolean) { - this.setState({ editMode: enabled === true }) - } +export function NoraItemRenderer({ studio, showStyleBase, piece }: INoraSuperRendererProps): JSX.Element { + const { t } = useTranslation() - render(): JSX.Element { - const { piece, t } = this.props + const actualPiece = RundownUtils.isAdLibPiece(piece) ? piece : piece.instance.piece - const actualPiece = RundownUtils.isAdLibPiece(piece) ? piece : piece.instance.piece + const [editMode, setEditMode] = useState(false) - const modalProps: IModalAttributes = { - title: actualPiece.name, - show: this.state.editMode, - onDiscard: () => { - this.setEditMode(false) - }, - } - - return ( - - -
-

{actualPiece.name}

-
- -
- - - -
-
- ) - } + const modalProps: IModalAttributes = { + title: actualPiece.name, + show: editMode, + onDiscard: () => { + setEditMode(false) + }, } -) + + return ( + + +
+

{actualPiece.name}

+
+ +
+ + + +
+
+ ) +} function isNoraItem(item: IAdLibListItem | PieceUi): boolean { const content = RundownUtils.isAdLibPiece(item) diff --git a/packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx b/packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx deleted file mode 100644 index db2eb2668a..0000000000 --- a/packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react' -import ClassNames from 'classnames' -import { - DashboardLayoutNextBreakTiming, - RundownLayoutBase, - RundownLayoutNextBreakTiming, -} from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { dashboardElementStyle } from './DashboardPanel' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { withTranslation } from 'react-i18next' -import { NextBreakTiming } from '../RundownView/RundownTiming/NextBreakTiming' - -interface INextBreakTimingPanelProps { - visible?: boolean - layout: RundownLayoutBase - panel: RundownLayoutNextBreakTiming - playlist: DBRundownPlaylist -} - -export class NextBreakTimingPanelInner extends React.Component> { - render(): JSX.Element { - const { panel, layout } = this.props - - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) - - return ( -
- -
- ) - } -} - -export const NextBreakTimingPanel = withTranslation()(NextBreakTimingPanelInner) diff --git a/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx b/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx index f614020cbf..c669e8eacc 100644 --- a/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import * as _ from 'underscore' import ClassNames from 'classnames' import { @@ -7,13 +6,13 @@ import { RundownLayoutNextInfo, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { dashboardElementStyle } from './DashboardPanel' import { Segments } from '../../collections' import { UIPartInstances } from '../Collections' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' interface INextInfoPanelProps { visible?: boolean @@ -22,57 +21,51 @@ interface INextInfoPanelProps { playlist: DBRundownPlaylist } -interface INextInfoPanelTrackedProps { - nextPartInstance?: PartInstance - nextSegment?: DBSegment -} +export function NextInfoPanel({ visible, layout, panel, playlist }: INextInfoPanelProps): JSX.Element { + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) -export class NextInfoPanelInner extends React.Component { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const showAny = - !this.props.panel.hideForDynamicallyInsertedParts || this.props.nextPartInstance?.orphaned !== 'adlib-part' - const segmentName = showAny && this.props.panel.showSegmentName && this.props.nextSegment?.name - const partTitle = showAny && this.props.panel.showPartTitle && this.props.nextPartInstance?.part.title - return ( -
-
- {showAny && this.props.panel.name} - {segmentName && {segmentName}} - {partTitle && {partTitle}} -
-
- ) - } -} + const nextPartInstanceId = playlist.nextPartInfo?.partInstanceId + const nextPartInstance = useTracker( + () => + nextPartInstanceId && + (UIPartInstances.findOne(nextPartInstanceId, { + projection: { + segmentId: 1, + orphaned: 1, + part: 1, + }, + }) as Pick), + [nextPartInstanceId] + ) -export const NextInfoPanel = withTracker( - (props: INextInfoPanelProps & INextInfoPanelTrackedProps) => { - let nextPartInstance: PartInstance | undefined = undefined - let nextSegment: DBSegment | undefined = undefined + const nextSegmentId = nextPartInstance?.segmentId + const nextSegment = useTracker( + () => nextSegmentId && (Segments.findOne(nextSegmentId, { projection: { name: 1 } }) as Pick), + [nextSegmentId] + ) - if (props.playlist.nextPartInfo) { - nextPartInstance = UIPartInstances.findOne(props.playlist.nextPartInfo.partInstanceId) - } - if (nextPartInstance) { - nextSegment = Segments.findOne(nextPartInstance.segmentId) - } - return { nextPartInstance, nextSegment } - }, - (_data, props: INextInfoPanelProps, nextProps: INextInfoPanelProps) => { - return !_.isEqual(props, nextProps) - } -)(NextInfoPanelInner) + const showAny = !panel.hideForDynamicallyInsertedParts || nextPartInstance?.orphaned !== 'adlib-part' + const segmentName = showAny && panel.showSegmentName && nextSegment?.name + const partTitle = showAny && panel.showPartTitle && nextPartInstance?.part.title + + return ( +
+
+ {showAny && panel.name} + {segmentName && {segmentName}} + {partTitle && {partTitle}} +
+
+ ) +} diff --git a/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx b/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx index ab8af0d09c..617f854099 100644 --- a/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx @@ -1,5 +1,3 @@ -import * as React from 'react' -import * as _ from 'underscore' import ClassNames from 'classnames' import { DashboardLayoutPartName, @@ -9,94 +7,74 @@ import { import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { findPieceInstanceToShowFromInstances, IFoundPieceInstance } from '../PieceIcons/utils' import { pieceIconSupportedLayers } from '../PieceIcons/PieceIcon' import { RundownUtils } from '../../lib/rundown' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { PieceInstances } from '../../collections' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' interface IPartNamePanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPartName playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } -interface IState {} - interface IPartNamePanelTrackedProps { - name?: string + partName?: string instanceToShow?: IFoundPieceInstance } -class PartNamePanelInner extends React.Component, IState> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t } = this.props - - const sourceLayerType = this.props.instanceToShow?.sourceLayer?.type - let backgroundSourceLayer = sourceLayerType ? RundownUtils.getSourceLayerClassName(sourceLayerType) : undefined - - if (!backgroundSourceLayer) { - backgroundSourceLayer = '' - } +export function PartNamePanel({ layout, panel, playlist, showStyleBase }: IPartNamePanelProps): JSX.Element { + const { t } = useTranslation() - return ( -
-
- - {this.props.panel.part === 'current' ? t('Current Part') : t('Next Part')} - - {this.props.name} -
-
- ) - } -} + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) -export const PartNamePanel = translateWithTracker( - (props) => { - const selectedPartInstanceId = - props.panel.part === 'current' - ? props.playlist.currentPartInfo?.partInstanceId - : props.playlist.nextPartInfo?.partInstanceId - let name: string | undefined - let instanceToShow: IFoundPieceInstance | undefined + const selectedPartInstanceId = + panel.part === 'current' ? playlist.currentPartInfo?.partInstanceId : playlist.nextPartInfo?.partInstanceId - if (selectedPartInstanceId) { - const selectedPartInstance = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { + const { partName, instanceToShow } = useTracker( + () => { + if (!selectedPartInstanceId || !panel.showPieceIconColor) return {} + const selectedPartInstance = RundownPlaylistClientUtil.getActivePartInstances(playlist, { _id: selectedPartInstanceId, })[0] - if (selectedPartInstance && props.panel.showPieceIconColor) { - name = selectedPartInstance.part?.title - const pieceInstances = PieceInstances.find({ partInstanceId: selectedPartInstance._id }).fetch() - instanceToShow = findPieceInstanceToShowFromInstances( - pieceInstances, - props.showStyleBase.sourceLayers, - pieceIconSupportedLayers - ) - } - } + if (!selectedPartInstance) return {} - return { - ...props, - name, - instanceToShow, - } - }, - (_data, props, nextProps) => { - return ( - !_.isEqual(props.panel, nextProps.panel) || - props.playlist.currentPartInfo?.partInstanceId !== nextProps.playlist.currentPartInfo?.partInstanceId || - props.playlist.nextPartInfo?.partInstanceId !== nextProps.playlist.nextPartInfo?.partInstanceId - ) + const partName = selectedPartInstance.part?.title + const pieceInstances = PieceInstances.find({ partInstanceId: selectedPartInstance._id }).fetch() + const instanceToShow = findPieceInstanceToShowFromInstances( + pieceInstances, + showStyleBase.sourceLayers, + pieceIconSupportedLayers + ) + return { partName, instanceToShow } + }, + [panel.showPieceIconColor, playlist._id, showStyleBase.sourceLayers], + {} + ) + + const sourceLayerType = instanceToShow?.sourceLayer?.type + let backgroundSourceLayer = sourceLayerType ? RundownUtils.getSourceLayerClassName(sourceLayerType) : undefined + + if (!backgroundSourceLayer) { + backgroundSourceLayer = '' } -)(PartNamePanelInner) + + return ( +
+
+ {panel.part === 'current' ? t('Current Part') : t('Next Part')} + {partName} +
+
+ ) +} diff --git a/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx b/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx index 00e00ddb98..5fcb7240c3 100644 --- a/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx @@ -1,13 +1,10 @@ -import * as React from 'react' -import * as _ from 'underscore' import { DashboardLayoutPartCountDown, RundownLayoutBase, RundownLayoutPartTiming, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' import { getAllowSpeaking, getAllowVibrating } from '../../lib/localStorage' @@ -16,9 +13,10 @@ import { CurrentPartElapsed } from '../RundownView/RundownTiming/CurrentPartElap import { getIsFilterActive } from '../../lib/rundownLayouts' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' +import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface IPartTimingPanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPartTiming playlist: DBRundownPlaylist @@ -26,63 +24,56 @@ interface IPartTimingPanelProps { } interface IPartTimingPanelTrackedProps { - livePart?: PartInstance + livePartId?: PartId active: boolean } -interface IState {} +export function PartTimingPanel({ layout, panel, playlist, showStyleBase }: IPartTimingPanelProps): JSX.Element { + const { t } = useTranslation() -class PartTimingPanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t, panel } = this.props + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) - return ( -
- - {!panel.hideLabel && ( - - {panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')} - - )} - {this.props.active && - (panel.timingType === 'count_down' ? ( - - ) : ( - - ))} - -
- ) - } -} + const { active, livePartId } = useTracker( + () => { + if (!playlist.currentPartInfo) return { active: false } + + const livePartId: PartId | undefined = RundownPlaylistClientUtil.getActivePartInstances(playlist, { + _id: playlist.currentPartInfo.partInstanceId, + })[0]?.part?._id -export const PartTimingPanel = translateWithTracker( - (props: IPartTimingPanelProps) => { - if (props.playlist.currentPartInfo) { - const livePart = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { - _id: props.playlist.currentPartInfo.partInstanceId, - })[0] - const { active } = getIsFilterActive(props.playlist, props.showStyleBase, props.panel) + const { active } = getIsFilterActive(playlist, showStyleBase, panel) - return { active, livePart } - } - return { active: false } - }, - (_data, props: IPartTimingPanelProps, nextProps: IPartTimingPanelProps) => { - return !_.isEqual(props, nextProps) - } -)(PartTimingPanelInner) + return { active, livePartId } + }, + [playlist, showStyleBase, panel], + { active: false } + ) + + return ( +
+ + {!panel.hideLabel && ( + + {panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')} + + )} + {active && + (panel.timingType === 'count_down' ? ( + + ) : ( + + ))} + +
+ ) +} diff --git a/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx b/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx index 1fa8d6c069..60360eb424 100644 --- a/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx @@ -41,7 +41,6 @@ export function PlaylistEndTimerPanel({ playlist, panel, layout }: Readonly
) diff --git a/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx b/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx index d8be2534f7..4e55942f16 100644 --- a/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import ClassNames from 'classnames' import { DashboardLayoutPlaylistName, @@ -8,69 +7,38 @@ import { import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Rundowns } from '../../collections' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' -import { logger } from '../../lib/logging' -import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' interface IPlaylistNamePanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPlaylistName playlist: DBRundownPlaylist } -interface IState {} - -interface IPlaylistNamePanelTrackedProps { - currentRundown?: Rundown -} - -class PlaylistNamePanelInner extends React.Component { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { panel } = this.props - - return ( -
-
- {this.props.playlist.name} - {this.props.panel.showCurrentRundownName && this.props.currentRundown && ( - {this.props.currentRundown.name} - )} -
+export function PlaylistNamePanel({ panel, layout, playlist }: IPlaylistNamePanelProps): JSX.Element { + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) + + const currentRundownId = playlist.currentPartInfo?.rundownId + const currentRundownName = useTracker(() => { + if (!panel.showCurrentRundownName || !currentRundownId) return undefined + const rundown = Rundowns.findOne(currentRundownId, { projection: { name: 1 } }) as Pick + return rundown?.name + }, [currentRundownId, panel.showCurrentRundownName]) + + return ( +
+
+ {playlist.name} + {currentRundownName && {currentRundownName}}
- ) - } +
+ ) } - -export const PlaylistNamePanel = withTracker( - (props: IPlaylistNamePanelProps) => { - if (props.playlist.currentPartInfo) { - const livePart: PartInstance = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { - _id: props.playlist.currentPartInfo.partInstanceId, - })[0] - if (!livePart) { - logger.warn( - `No PartInstance found for PartInstanceId: ${props.playlist.currentPartInfo.partInstanceId} in Playlist: ${props.playlist._id}` - ) - return {} - } - const currentRundown = Rundowns.findOne({ _id: livePart.rundownId, playlistId: props.playlist._id }) - - return { - currentRundown, - } - } - - return {} - } -)(PlaylistNamePanelInner) diff --git a/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx b/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx index cd3478378c..8c26a654da 100644 --- a/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx +++ b/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import ClassNames from 'classnames' import { DashboardLayoutSegmentName, @@ -8,49 +7,41 @@ import { import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' interface ISegmentNamePanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutSegmentName playlist: DBRundownPlaylist } -interface IState {} +export function SegmentNamePanel({ layout, panel, playlist }: ISegmentNamePanelProps): JSX.Element { + const { t } = useTranslation() -interface ISegmentNamePanelTrackedProps { - name?: string -} + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) -class SegmentNamePanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t, panel } = this.props + const segmentName = useTracker(() => getSegmentName(panel.segment, playlist), [panel.segment, playlist]) - return ( -
-
- - {this.props.panel.segment === 'current' ? t('Current Segment') : t('Next Segment')} - - {this.props.name} -
+ return ( +
+
+ + {panel.segment === 'current' ? t('Current Segment') : t('Next Segment')} + + {segmentName}
- ) - } +
+ ) } function getSegmentName(selectedSegment: 'current' | 'next', playlist: DBRundownPlaylist): string | undefined { @@ -92,14 +83,3 @@ function getSegmentName(selectedSegment: 'current' | 'next', playlist: DBRundown return nextSegment?.name } } - -export const SegmentNamePanel = translateWithTracker( - (props) => { - const name: string | undefined = getSegmentName(props.panel.segment, props.playlist) - - return { - ...props, - name, - } - } -)(SegmentNamePanelInner) diff --git a/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx b/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx index 898bd6ffb3..1075acf95c 100644 --- a/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import * as _ from 'underscore' import ClassNames from 'classnames' import { @@ -6,7 +5,7 @@ import { RundownLayoutBase, RundownLayoutSegmentTiming, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../lib/rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' @@ -23,6 +22,7 @@ import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyle import { PartId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylistCollectionUtil } from '../../collections/rundownPlaylistUtil' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' interface ISegmentTimingPanelProps { visible?: boolean @@ -38,49 +38,44 @@ interface ISegmentTimingPanelTrackedProps { active: boolean } -interface IState {} +function SegmentTimingPanelInner({ + layout, + panel, + liveSegment, + parts, + active, +}: ISegmentTimingPanelProps & ISegmentTimingPanelTrackedProps) { + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) + const { t } = useTranslation() -class SegmentTimingPanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t, panel } = this.props - - return ( -
+ + {!panel.hideLabel && ( + + {panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')} + )} - style={isDashboardLayout ? dashboardElementStyle(this.props.panel as DashboardLayoutSegmentCountDown) : {}} - > - - {!panel.hideLabel && ( - - {panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')} - - )} - {this.props.active && this.props.liveSegment && this.props.parts && ( - - )} - -
- ) - } + {active && liveSegment && parts && ( + + )} + +
+ ) } -export const SegmentTimingPanel = translateWithTracker< - ISegmentTimingPanelProps, - IState, - ISegmentTimingPanelTrackedProps ->( +export const SegmentTimingPanel = withTracker( (props: ISegmentTimingPanelProps) => { if (props.playlist.currentPartInfo) { const livePart = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { diff --git a/packages/webui/src/client/ui/Status/DebugState.tsx b/packages/webui/src/client/ui/Status/DebugState.tsx index 8641e3269c..3c57d13e79 100644 --- a/packages/webui/src/client/ui/Status/DebugState.tsx +++ b/packages/webui/src/client/ui/Status/DebugState.tsx @@ -1,71 +1,60 @@ -import * as React from 'react' -import { Translated } from '../../lib/ReactMeteorData/react-meteor-data' -import * as reacti18next from 'react-i18next' +import { useTranslation } from 'react-i18next' interface IDebugStateTableProps { debugState: object } -interface IDebugStateTableState {} -export const DebugStateTable = reacti18next.withTranslation()( - class DebugStateTable extends React.Component, IDebugStateTableState> { - constructor(props: Translated) { - super(props) - } +export function DebugStateTable({ debugState }: IDebugStateTableProps): JSX.Element { + const { t } = useTranslation() - private getDebugStateTableBody(debugState: object) { - /** - * Flattens object such that deeply-nested keys are moved to the top-level and are prefixed by - * their parent keys. - * - * # Example - * - * { "key1": { "key2": [ { "key3": "example" } ] } } - * - * becomes - * - * { "key1.key2.0.key3": "example" } - * @param acc Accumulator object, should be passed an empty object to begin - * @param obj Object to recurse - * @param currentKey Current key within the object being recursed (initially blank) - * @returns "Flattened" object - */ - function toDotNotation(acc: any, obj: any, currentKey?: string): object { - for (const key in obj) { - const value = obj[key] - const newKey = currentKey ? currentKey + '.' + key : key // joined key with dot - if (value && typeof value === 'object' && Object.keys(value).length) { - acc = toDotNotation(acc, value, newKey) // it's a nested object, so do it again - } else { - acc[newKey] = value // it's not an object, so set the property - } - } + return ( +
+ + + {getDebugStateTableBody(debugState)} +
+
+ ) +} - return acc +function getDebugStateTableBody(debugState: object) { + /** + * Flattens object such that deeply-nested keys are moved to the top-level and are prefixed by + * their parent keys. + * + * # Example + * + * { "key1": { "key2": [ { "key3": "example" } ] } } + * + * becomes + * + * { "key1.key2.0.key3": "example" } + * @param acc Accumulator object, should be passed an empty object to begin + * @param obj Object to recurse + * @param currentKey Current key within the object being recursed (initially blank) + * @returns "Flattened" object + */ + function toDotNotation(acc: any, obj: any, currentKey?: string): object { + for (const key in obj) { + const value = obj[key] + const newKey = currentKey ? currentKey + '.' + key : key // joined key with dot + if (value && typeof value === 'object' && Object.keys(value).length) { + acc = toDotNotation(acc, value, newKey) // it's a nested object, so do it again + } else { + acc[newKey] = value // it's not an object, so set the property } - - const objectInDotNotation = toDotNotation({}, debugState) - return Object.entries(objectInDotNotation).map(([key, value]) => { - return ( - - {key} - {JSON.stringify(value)} - - ) - }) } - render(): JSX.Element { - const { t } = this.props - - return ( -
- - - {this.getDebugStateTableBody(this.props.debugState)} -
-
- ) - } + return acc } -) + + const objectInDotNotation = toDotNotation({}, debugState) + return Object.entries(objectInDotNotation).map(([key, value]) => { + return ( + + {key} + {JSON.stringify(value)} + + ) + }) +} From 8dc29c9d5b84895241c5fd6897dc8e56f9dcb3c0 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 14:22:11 +0000 Subject: [PATCH 10/15] chore: refactor EditAttribute to hooks --- .../webui/src/client/lib/EditAttribute.tsx | 991 ++++++++---------- packages/webui/src/client/lib/ModalDialog.tsx | 6 +- .../src/client/ui/Settings/Migration.tsx | 4 +- 3 files changed, 438 insertions(+), 563 deletions(-) diff --git a/packages/webui/src/client/lib/EditAttribute.tsx b/packages/webui/src/client/lib/EditAttribute.tsx index 7138d7645b..0fb0b5c1b6 100644 --- a/packages/webui/src/client/lib/EditAttribute.tsx +++ b/packages/webui/src/client/lib/EditAttribute.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import * as _ from 'underscore' -import { withTracker } from './ReactMeteorData/react-meteor-data' - +import { useTracker } from './ReactMeteorData/react-meteor-data' import { MultiSelect, MultiSelectEvent, MultiSelectOptions } from './multiSelect' import ClassNames from 'classnames' import { ColorPickerEvent, ColorPicker } from './colorPicker' @@ -11,11 +10,12 @@ import { MongoCollection } from '../collections/lib' import { CheckboxControl } from './Components/Checkbox' import { TextInputControl } from './Components/TextInput' import { IntInputControl } from './Components/IntInput' -import { DropdownInputControl, DropdownInputOption, getDropdownInputOptions } from './Components/DropdownInput' +import { DropdownInputControl, getDropdownInputOptions } from './Components/DropdownInput' import { FloatInputControl } from './Components/FloatInput' import { joinLines, MultiLineTextInputControl, splitValueIntoLines } from './Components/MultiLineTextInput' import { JsonTextInputControl, tryParseJson } from './Components/JsonTextInput' import { ToggleSwitchControl } from './Components/ToggleSwitch' +import { useCallback, useMemo, useState } from 'react' interface IEditAttribute extends IEditAttributeBaseProps { type: EditAttributeType @@ -70,18 +70,17 @@ export class EditAttribute extends React.Component { } } -interface IEditAttributeBaseProps { +export interface IEditAttributeBaseProps { updateOnKey?: boolean attribute?: string collection?: MongoCollection - myObject?: any obj?: any options?: any optionsAreNumbers?: boolean className?: string modifiedClassName?: string invalidClassName?: string - updateFunction?: (edit: EditAttributeBase, newValue: any) => void + updateFunction?: (editProps: IEditAttributeBaseProps, newValue: any) => void overrideDisplayValue?: any label?: string mutateDisplayValue?: (v: any) => any @@ -91,601 +90,477 @@ interface IEditAttributeBaseProps { /** Defaults to string */ arrayType?: 'boolean' | 'int' | 'float' | 'string' } -interface IEditAttributeBaseState { - value: any - valueError: boolean - editing: boolean -} -export class EditAttributeBase extends React.Component { - constructor(props: IEditAttributeBaseProps) { - super(props) - - this.state = { - value: this.getAttribute(), - valueError: false, - editing: false, - } - this.handleEdit = this.handleEdit.bind(this) - this.handleUpdate = this.handleUpdate.bind(this) - this.handleDiscard = this.handleDiscard.bind(this) - } - /** Update the temporary value of this field, optionally saving a value */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleEdit(inputValue: any, storeValue?: any): void { - this.setState({ - value: inputValue, - editing: true, - }) - if (this.props.updateOnKey) { - this.updateValue(storeValue ?? inputValue) - } - } - /** Update and save the value of this field */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleUpdate(inputValue: any, storeValue?: any): void { - this.handleUpdateButDontSave(inputValue) - this.updateValue(storeValue ?? inputValue) - } - /** Update the temporary value of this field, and save it */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleUpdateEditing(newValue: any): void { - this.handleUpdateButDontSave(newValue, true) - this.updateValue(newValue) - } - /** Update the temporary value of this field, marking whether is being edited */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleUpdateButDontSave(newValue: any, editing = false): void { - this.setState({ - value: newValue, - editing, - }) - } - /** Discard the temporary value of this field */ - protected handleDiscard(): void { - this.setState({ - value: this.getAttribute(), - editing: false, - }) - } - private deepAttribute(obj0: any, attr0: string | undefined): any { - // Returns a value deep inside an object - // Example: deepAttribute(company,"ceo.address.street"); +function EditAttributeText(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeMultilineText(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (value: string[]) => { + stateHelper.handleUpdate(joinLines(value)) // as single string + }, + [stateHelper.handleUpdate] + ) + + return ( + + ) +} +function EditAttributeInt(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeFloat(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeCheckbox(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeToggle(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeDropdown(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const options = useMemo(() => getDropdownInputOptions(props.options), [props.options]) + + const handleChange = useCallback( + (value: string) => { + stateHelper.handleUpdate(props.optionsAreNumbers ? parseInt(value, 10) : value) + }, + [stateHelper.handleUpdate, props.optionsAreNumbers] + ) + + return ( + + ) +} +function EditAttributeDropdownText(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const options = useMemo(() => getDropdownInputOptions(props.options), [props.options]) + + return ( + + ) +} - const f = (obj: any, attr: string): any => { - if (obj) { - const attributes = attr.split('.') +interface EditAttributeMultiSelectOptionsResult { + options: MultiSelectOptions + currentOptionMissing: boolean +} - if (attributes.length > 1) { - const outerAttr = attributes.shift() as string - const innerAttrs = attributes.join('.') +function EditAttributeMultiSelect(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const currentValue = stateHelper.getAttributeValue() + const options = getMultiselectOptions(props.options, currentValue, true) + + const handleChange = useCallback( + (event: MultiSelectEvent) => stateHelper.handleUpdate(event.selectedValues), + [stateHelper.handleUpdate] + ) + + return ( + + ) +} - return f(obj[outerAttr], innerAttrs) - } else { - return obj[attributes[0]] - } +function getMultiselectOptions( + rawOptions: any, + currentValue: any, + addOptionsForCurrentValue?: boolean +): EditAttributeMultiSelectOptionsResult { + const options: MultiSelectOptions = {} + + if (Array.isArray(rawOptions)) { + // is it an enum? + for (const val of rawOptions) { + if (typeof val === 'object') { + options[val.value] = { value: val.name } } else { - return obj + options[val] = { value: val } } } - return f(obj0, attr0 || '') - } - protected getAttribute(): any { - let v = null - if (this.props.overrideDisplayValue !== undefined) { - v = this.props.overrideDisplayValue - } else { - v = this.deepAttribute(this.props.myObject, this.props.attribute) - } - return this.props.mutateDisplayValue ? this.props.mutateDisplayValue(v) : v - } - - protected getEditAttribute(): any { - return this.state.editing ? this.state.value : this.getAttribute() - } - private updateValue(newValue: any) { - if (this.props.mutateUpdateValue) { - try { - newValue = this.props.mutateUpdateValue(newValue) - this.setState({ - valueError: false, - }) - } catch (e) { - this.setState({ - valueError: true, - editing: true, - }) - return + } else if (typeof rawOptions === 'object') { + // Is options an enum? + const keys = Object.keys(rawOptions) + const first = rawOptions[keys[0]] + if (rawOptions[first] + '' === keys[0] + '') { + // is an enum, only pick + for (const key in rawOptions) { + if (!_.isNaN(parseInt(key, 10))) { + // key is a number (the key) + const enumValue = rawOptions[key] + const enumKey = rawOptions[enumValue] + options[enumKey] = { value: enumValue } + } } - } - - if (this.props.updateFunction && typeof this.props.updateFunction === 'function') { - this.props.updateFunction(this, newValue) } else { - if (this.props.collection && this.props.attribute) { - if (newValue === undefined) { - const m: Record = {} - m[this.props.attribute] = 1 - this.props.collection.update(this.props.obj._id, { $unset: m }) + for (const key in rawOptions) { + const val = rawOptions[key] + if (Array.isArray(val)) { + options[key] = { value: val } } else { - const m: Record = {} - m[this.props.attribute] = newValue - this.props.collection.update(this.props.obj._id, { $set: m }) + options[val] = { value: key + ': ' + val } } } } } -} -function wrapEditAttribute(newClass: any) { - return withTracker((props: IEditAttributeBaseProps) => { - // These properties will be exposed under this.props - // Note that these properties are reactively recalculated - return { - myObject: props.collection ? props.collection.findOne(props.obj._id) : props.obj || {}, - } - })(newClass) -} -const EditAttributeText = wrapEditAttribute( - class EditAttributeText extends EditAttributeBase { - constructor(props: any) { - super(props) + const missingOptions = Array.isArray(currentValue) ? currentValue.filter((v) => !(v in options)) : [] - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: string) { - this.handleUpdate(value) - } - render(): JSX.Element { - return ( - - ) - } + if (addOptionsForCurrentValue) { + missingOptions.forEach((option) => { + options[option] = { value: `${option}`, className: 'option-missing' } + }) } -) -const EditAttributeMultilineText = wrapEditAttribute( - class EditAttributeMultilineText extends EditAttributeBase { - constructor(props: any) { - super(props) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: string[]) { - this.handleUpdate(joinLines(value)) // as single string - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeInt = wrapEditAttribute( - class EditAttributeInt extends EditAttributeBase { - constructor(props: any) { - super(props) + return { options, currentOptionMissing: !!missingOptions.length } +} - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: number) { - this.handleUpdate(value) - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeFloat = wrapEditAttribute( - class EditAttributeFloat extends EditAttributeBase { - constructor(props: any) { - super(props) +function EditAttributeJson(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (value: object) => { + const storeValue = props.storeJsonAsObject ? value : JSON.stringify(value, undefined, 2) + stateHelper.handleUpdate(storeValue) + }, + [stateHelper.handleUpdate, props.storeJsonAsObject] + ) + + const value = props.storeJsonAsObject + ? stateHelper.getAttributeValue() + : tryParseJson(stateHelper.getAttributeValue())?.parsed + + return ( + + ) +} +function EditAttributeArray(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: number) { - this.handleUpdate(value) - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeCheckbox = wrapEditAttribute( - class EditAttributeCheckbox extends EditAttributeBase { - constructor(props: any) { - super(props) + const [editingValue, setEditingValue] = useState(null) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: boolean) { - this.handleUpdate(value) - } - render(): JSX.Element { - const classNames = _.compact([ - this.props.className, - this.state.editing ? this.props.modifiedClassName : undefined, - ]).join(' ') - - return ( - - ) - } - } -) -const EditAttributeToggle = wrapEditAttribute( - class EditAttributeToggle extends EditAttributeBase { - constructor(props: any) { - super(props) - } - private isChecked() { - return !!this.getEditAttribute() - } + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const v = event.target.value - render(): JSX.Element { - return ( - - ) - } - } -) + setEditingValue(v) -const EditAttributeDropdown = wrapEditAttribute( - class EditAttributeDropdown extends EditAttributeBase { - constructor(props: any) { - super(props) + const arrayObj = stringIsArray(v, props.arrayType) + if (arrayObj) { + if (props.updateOnKey) { + stateHelper.handleUpdate(arrayObj.parsed) + } + stateHelper.setValueError(false) + } + }, + [setEditingValue, props.arrayType, props.updateOnKey, stateHelper.handleUpdate, stateHelper.setValueError] + ) + const handleBlur = useCallback( + (event: React.FocusEvent) => { + const v = event.target.value - this.handleChange = this.handleChange.bind(this) - } - handleChange(value: string) { - this.handleUpdate(this.props.optionsAreNumbers ? parseInt(value, 10) : value) - } - render(): JSX.Element { - const options = getDropdownInputOptions(this.props.options) - - return ( - - ) - } + setEditingValue(v) + + const arrayObj = stringIsArray(v, props.arrayType) + if (arrayObj) { + stateHelper.handleUpdate(arrayObj.parsed) + stateHelper.setValueError(false) + } else { + stateHelper.setValueError(true) + } + }, + [setEditingValue, props.arrayType, stateHelper.handleUpdate, stateHelper.setValueError] + ) + const handleEscape = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setEditingValue(null) + } + }, + [setEditingValue] + ) + + let currentValueString = stateHelper.getAttributeValue() + if (Array.isArray(currentValueString)) { + currentValueString = currentValueString.join(', ') + } else { + currentValueString = '' } -) -const EditAttributeDropdownText = wrapEditAttribute( - class EditAttributeDropdownText extends EditAttributeBase { - private getOptions(): DropdownInputOption[] { - return getDropdownInputOptions(this.props.options) - } - render(): JSX.Element { - return ( - - ) + + return ( + + ) +} +function stringIsArray(strOrg: string, arrayType: IEditAttributeBaseProps['arrayType']): { parsed: any[] } | false { + if (!(strOrg + '').trim().length) return { parsed: [] } + + const values: any[] = [] + const strs = (strOrg + '').split(',') + + for (const str of strs) { + // Check that the values in the array are of the right type: + + if (arrayType === 'boolean') { + const parsed = JSON.parse(str) + if (typeof parsed !== 'boolean') return false // type check failed + values.push(parsed) + } else if (arrayType === 'int') { + const parsed = parseInt(str, 10) + + if (Number.isNaN(parsed)) return false // type check failed + values.push(parsed) + } else if (arrayType === 'float') { + const parsed = parseFloat(str) + if (Number.isNaN(parsed)) return false // type check failed + values.push(parsed) + } else { + // else this.props.arrayType is 'string' + const parsed = str + '' + if (typeof parsed !== 'string') return false // type check failed + values.push(parsed.trim()) } } -) - -interface EditAttributeMultiSelectOptionsResult { - options: MultiSelectOptions - currentOptionMissing: boolean + return { parsed: values } +} +function EditAttributeColorPicker(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (event: ColorPickerEvent) => stateHelper.handleUpdate(event.selectedValue), + [stateHelper.handleUpdate] + ) + + return ( + + ) +} +function EditAttributeIconPicker(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (event: IconPickerEvent) => stateHelper.handleUpdate(event.selectedValue), + [stateHelper.handleUpdate] + ) + + return ( + + ) } -const EditAttributeMultiSelect = wrapEditAttribute( - class EditAttributeMultiSelect extends EditAttributeBase { - constructor(props: any) { - super(props) - - this.handleChange = this.handleChange.bind(this) - } - handleChange(event: MultiSelectEvent) { - this.handleUpdate(event.selectedValues) - } - getOptions(addOptionsForCurrentValue?: boolean): EditAttributeMultiSelectOptionsResult { - const options: MultiSelectOptions = {} - - if (Array.isArray(this.props.options)) { - // is it an enum? - for (const val of this.props.options) { - if (typeof val === 'object') { - options[val.value] = { value: val.name } - } else { - options[val] = { value: val } - } - } - } else if (typeof this.props.options === 'object') { - // Is options an enum? - const keys = Object.keys(this.props.options) - const first = this.props.options[keys[0]] - if (this.props.options[first] + '' === keys[0] + '') { - // is an enum, only pick - for (const key in this.props.options) { - if (!_.isNaN(parseInt(key, 10))) { - // key is a number (the key) - const enumValue = this.props.options[key] - const enumKey = this.props.options[enumValue] - options[enumKey] = { value: enumValue } - } - } - } else { - for (const key in this.props.options) { - const val = this.props.options[key] - if (Array.isArray(val)) { - options[key] = { value: val } - } else { - options[val] = { value: key + ': ' + val } - } - } - } - } +interface EditAttributeStateHelper { + props: Readonly + myObject: any - const currentValue = this.getAttribute() - const missingOptions = Array.isArray(currentValue) ? currentValue.filter((v) => !(v in options)) : [] + valueError: boolean + setValueError: (value: boolean) => void - if (addOptionsForCurrentValue) { - missingOptions.forEach((option) => { - options[option] = { value: `${option}`, className: 'option-missing' } - }) - } + getAttributeValue: () => any + handleUpdate: (inputValue: any, storeValue?: any) => void +} - return { options, currentOptionMissing: !!missingOptions.length } - } - render(): JSX.Element { - const options = this.getOptions(true) - return ( - - ) - } - } -) +function useEditAttributeStateHelper(props: IEditAttributeBaseProps): EditAttributeStateHelper { + const [valueError, setValueError] = useState(false) -const EditAttributeJson = wrapEditAttribute( - class EditAttributeJson extends EditAttributeBase { - constructor(props: any) { - super(props) + const myObject = useTracker( + () => (props.collection ? props.collection.findOne(props.obj._id) : props.obj || {}), + [props.collection, props.obj] + ) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: object) { - const storeValue = this.props.storeJsonAsObject ? value : JSON.stringify(value, undefined, 2) - this.handleUpdate(storeValue) - } - render(): JSX.Element { - const value = this.props.storeJsonAsObject ? this.getAttribute() : tryParseJson(this.getAttribute())?.parsed - - return ( - - ) - } - } -) -const EditAttributeArray = wrapEditAttribute( - class EditAttributeArray extends EditAttributeBase { - constructor(props: any) { - super(props) - - this.handleChange = this.handleChange.bind(this) - this.handleBlur = this.handleBlur.bind(this) - this.handleEscape = this.handleEscape.bind(this) + const getAttributeValue = useCallback((): any => { + let v = null + if (props.overrideDisplayValue !== undefined) { + v = props.overrideDisplayValue + } else { + v = deepAttribute(myObject, props.attribute) } - isArray(strOrg: string): { parsed: any[] } | false { - if (!(strOrg + '').trim().length) return { parsed: [] } - - const values: any[] = [] - const strs = (strOrg + '').split(',') - - for (const str of strs) { - // Check that the values in the array are of the right type: - - if (this.props.arrayType === 'boolean') { - const parsed = JSON.parse(str) - if (typeof parsed !== 'boolean') return false // type check failed - values.push(parsed) - } else if (this.props.arrayType === 'int') { - const parsed = parseInt(str, 10) - - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else if (this.props.arrayType === 'float') { - const parsed = parseFloat(str) - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else { - // else this.props.arrayType is 'string' - const parsed = str + '' - if (typeof parsed !== 'string') return false // type check failed - values.push(parsed.trim()) + return props.mutateDisplayValue ? props.mutateDisplayValue(v) : v + }, [props.overrideDisplayValue, props.mutateDisplayValue, deepAttribute, myObject, props.attribute]) + + /** Update and save the value of this field */ + const handleUpdate = useCallback( + (newValue: any) => { + if (props.mutateUpdateValue) { + try { + newValue = props.mutateUpdateValue(newValue) + setValueError(false) + } catch (e) { + setValueError(true) + + return } } - return { parsed: values } - } - handleChange(event: React.ChangeEvent) { - const v = event.target.value - const arrayObj = this.isArray(v) - if (arrayObj) { - this.handleEdit(v, arrayObj.parsed) - this.setState({ - valueError: false, - }) + if (props.updateFunction && typeof props.updateFunction === 'function') { + props.updateFunction(props, newValue) } else { - this.handleUpdateButDontSave(v, true) + if (props.collection && props.attribute) { + if (newValue === undefined) { + const m: Record = {} + m[props.attribute] = 1 + props.collection.update(props.obj._id, { $unset: m }) + } else { + const m: Record = {} + m[props.attribute] = newValue + props.collection.update(props.obj._id, { $set: m }) + } + } } - } - handleBlur(event: React.FocusEvent) { - const v = event.target.value + }, + [props, props.mutateUpdateValue, props.updateFunction, props.collection, props.attribute, props.obj?._id] + ) - const arrayObj = this.isArray(v) - if (arrayObj) { - this.handleUpdate(v, arrayObj.parsed) - this.setState({ - valueError: false, - }) - } else { - this.handleUpdateButDontSave(v, true) - this.setState({ - valueError: true, - }) - } - } - handleEscape(e: React.KeyboardEvent) { - if (e.key === 'Escape') { - this.handleDiscard() - } - } - getAttribute() { - const value = super.getAttribute() - if (Array.isArray(value)) { - return value.join(', ') - } else { - return '' - } - } - render(): JSX.Element { - return ( - - ) - } - } -) + return { props, myObject, valueError, setValueError, getAttributeValue, handleUpdate } +} -const EditAttributeColorPicker = wrapEditAttribute( - class EditAttributeColorPicker extends EditAttributeBase { - constructor(props: any) { - super(props) +function deepAttribute(obj0: any, attr0: string | undefined): any { + // Returns a value deep inside an object + // Example: deepAttribute(company,"ceo.address.street"); - this.handleChange = this.handleChange.bind(this) - } - handleChange(event: ColorPickerEvent) { - this.handleUpdate(event.selectedValue) - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeIconPicker = wrapEditAttribute( - class extends EditAttributeBase { - constructor(props: any) { - super(props) + const f = (obj: any, attr: string): any => { + if (obj) { + const attributes = attr.split('.') - this.handleChange = this.handleChange.bind(this) - } - handleChange(event: IconPickerEvent) { - this.handleUpdate(event.selectedValue) - } - render(): JSX.Element { - return ( - - ) + if (attributes.length > 1) { + const outerAttr = attributes.shift() as string + const innerAttrs = attributes.join('.') + + return f(obj[outerAttr], innerAttrs) + } else { + return obj[attributes[0]] + } + } else { + return obj } } -) + return f(obj0, attr0 || '') +} diff --git a/packages/webui/src/client/lib/ModalDialog.tsx b/packages/webui/src/client/lib/ModalDialog.tsx index 0243afef23..089625e53f 100644 --- a/packages/webui/src/client/lib/ModalDialog.tsx +++ b/packages/webui/src/client/lib/ModalDialog.tsx @@ -12,7 +12,7 @@ import { logger } from './logging' import * as _ from 'underscore' import { withTranslation } from 'react-i18next' import { Translated } from './ReactMeteorData/ReactMeteorData' -import { EditAttribute, EditAttributeType, EditAttributeBase } from './EditAttribute' +import { EditAttribute, EditAttributeType, IEditAttributeBaseProps } from './EditAttribute' import { Settings } from '../lib/Settings' interface IModalDialogAttributes { @@ -88,8 +88,8 @@ export function ModalDialog({ callback(e, inputResult.current) } - function updateInput(edit: EditAttributeBase, newValue: any) { - inputResult.current[edit.props.attribute || ''] = newValue + function updateInput(editProps: IEditAttributeBaseProps, newValue: any) { + inputResult.current[editProps.attribute || ''] = newValue } function emulateClick(e: React.KeyboardEvent) { diff --git a/packages/webui/src/client/ui/Settings/Migration.tsx b/packages/webui/src/client/ui/Settings/Migration.tsx index 20f77b6a65..234371ebed 100644 --- a/packages/webui/src/client/ui/Settings/Migration.tsx +++ b/packages/webui/src/client/ui/Settings/Migration.tsx @@ -12,7 +12,7 @@ import { } from '@sofie-automation/meteor-lib/dist/api/migration' import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import * as _ from 'underscore' -import { EditAttribute, EditAttributeBase } from '../../lib/EditAttribute' +import { EditAttribute } from '../../lib/EditAttribute' import { MeteorCall } from '../../lib/meteorApi' import { checkForOldDataAndCleanUp } from './SystemManagement' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' @@ -232,7 +232,7 @@ export const MigrationView = translateWithTracker className="input-full mtxs" options={manualInput.dropdownOptions} overrideDisplayValue={value} - updateFunction={(_edit: EditAttributeBase, newValue: any) => { + updateFunction={(_edit, newValue: any) => { if (manualInput.attribute) { const inputValues = this.state.inputValues if (!inputValues[stepId]) inputValues[stepId] = {} From 8c5c93355ce6759a47e9626b593f480e7df80309 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 14:22:53 +0000 Subject: [PATCH 11/15] chore: remove unused EditAttribute 'array' type --- .../webui/src/client/lib/EditAttribute.tsx | 108 ------------------ 1 file changed, 108 deletions(-) diff --git a/packages/webui/src/client/lib/EditAttribute.tsx b/packages/webui/src/client/lib/EditAttribute.tsx index 0fb0b5c1b6..96ae20cea1 100644 --- a/packages/webui/src/client/lib/EditAttribute.tsx +++ b/packages/webui/src/client/lib/EditAttribute.tsx @@ -33,7 +33,6 @@ export type EditAttributeType = | 'json' | 'colorpicker' | 'iconpicker' - | 'array' export class EditAttribute extends React.Component { render(): JSX.Element { if (this.props.type === 'text') { @@ -60,8 +59,6 @@ export class EditAttribute extends React.Component { return } else if (this.props.type === 'iconpicker') { return - } else if (this.props.type === 'array') { - return } else { assertNever(this.props.type) } @@ -335,111 +332,6 @@ function EditAttributeJson(props: IEditAttributeBaseProps) { /> ) } -function EditAttributeArray(props: IEditAttributeBaseProps) { - const stateHelper = useEditAttributeStateHelper(props) - - const [editingValue, setEditingValue] = useState(null) - - const handleChange = useCallback( - (event: React.ChangeEvent) => { - const v = event.target.value - - setEditingValue(v) - - const arrayObj = stringIsArray(v, props.arrayType) - if (arrayObj) { - if (props.updateOnKey) { - stateHelper.handleUpdate(arrayObj.parsed) - } - stateHelper.setValueError(false) - } - }, - [setEditingValue, props.arrayType, props.updateOnKey, stateHelper.handleUpdate, stateHelper.setValueError] - ) - const handleBlur = useCallback( - (event: React.FocusEvent) => { - const v = event.target.value - - setEditingValue(v) - - const arrayObj = stringIsArray(v, props.arrayType) - if (arrayObj) { - stateHelper.handleUpdate(arrayObj.parsed) - stateHelper.setValueError(false) - } else { - stateHelper.setValueError(true) - } - }, - [setEditingValue, props.arrayType, stateHelper.handleUpdate, stateHelper.setValueError] - ) - const handleEscape = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setEditingValue(null) - } - }, - [setEditingValue] - ) - - let currentValueString = stateHelper.getAttributeValue() - if (Array.isArray(currentValueString)) { - currentValueString = currentValueString.join(', ') - } else { - currentValueString = '' - } - - return ( - - ) -} -function stringIsArray(strOrg: string, arrayType: IEditAttributeBaseProps['arrayType']): { parsed: any[] } | false { - if (!(strOrg + '').trim().length) return { parsed: [] } - - const values: any[] = [] - const strs = (strOrg + '').split(',') - - for (const str of strs) { - // Check that the values in the array are of the right type: - - if (arrayType === 'boolean') { - const parsed = JSON.parse(str) - if (typeof parsed !== 'boolean') return false // type check failed - values.push(parsed) - } else if (arrayType === 'int') { - const parsed = parseInt(str, 10) - - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else if (arrayType === 'float') { - const parsed = parseFloat(str) - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else { - // else this.props.arrayType is 'string' - const parsed = str + '' - if (typeof parsed !== 'string') return false // type check failed - values.push(parsed.trim()) - } - } - return { parsed: values } -} function EditAttributeColorPicker(props: IEditAttributeBaseProps) { const stateHelper = useEditAttributeStateHelper(props) From 8a109142ad5abb308121aaccafccc8b4df3092fa Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Thu, 19 Dec 2024 14:00:51 +0100 Subject: [PATCH 12/15] chore: update tsr --- meteor/yarn.lock | 10 +++++----- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 22 +++++++++++----------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index f9768f98ea..aa6c283780 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1240,7 +1240,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": v4.2.0 - timeline-state-resolver-types: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 + timeline-state-resolver-types: 9.2.0-nightly-release52-20241219-123204-90290cef1.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -10167,12 +10167,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0": - version: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 - resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0" +"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20241219-123204-90290cef1.0": + version: 9.2.0-nightly-release52-20241219-123204-90290cef1.0 + resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20241219-123204-90290cef1.0" dependencies: tslib: ^2.6.3 - checksum: c041363201bcfc0daac2ebca021b09fddc1f5b12fdeb932d9c19bfadc3ee308aa81f36c74c005edad2e756ed1c6465de779bfca5ed63ffd940878bf015497231 + checksum: 3c7a6fa15aa6d7b16efe8bf18ac187064e54e3a1e9dc284280a39c0256e6104d356095ea67495c7a5a3c1e1f408ea86a86094dd19972ba3e161acc6b1eab38fa languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 591e2d1d34..81454f98be 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.52.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.2.0-nightly-release52-20240923-122840-58cfbb259.0", + "timeline-state-resolver": "9.2.0-nightly-release52-20241219-123204-90290cef1.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 8a7a0ad996..b0c314bead 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "v4.2.0", - "timeline-state-resolver-types": "9.2.0-nightly-release52-20240923-122840-58cfbb259.0", + "timeline-state-resolver-types": "9.2.0-nightly-release52-20241219-123204-90290cef1.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 5928b1d4cc..815234d98b 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5281,7 +5281,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": v4.2.0 - timeline-state-resolver-types: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 + timeline-state-resolver-types: 9.2.0-nightly-release52-20241219-123204-90290cef1.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -21709,7 +21709,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.52.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 + timeline-state-resolver: 9.2.0-nightly-release52-20241219-123204-90290cef1.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -26031,18 +26031,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0": - version: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 - resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0" +"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20241219-123204-90290cef1.0": + version: 9.2.0-nightly-release52-20241219-123204-90290cef1.0 + resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20241219-123204-90290cef1.0" dependencies: tslib: ^2.6.3 - checksum: c041363201bcfc0daac2ebca021b09fddc1f5b12fdeb932d9c19bfadc3ee308aa81f36c74c005edad2e756ed1c6465de779bfca5ed63ffd940878bf015497231 + checksum: 3c7a6fa15aa6d7b16efe8bf18ac187064e54e3a1e9dc284280a39c0256e6104d356095ea67495c7a5a3c1e1f408ea86a86094dd19972ba3e161acc6b1eab38fa languageName: node linkType: hard -"timeline-state-resolver@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0": - version: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 - resolution: "timeline-state-resolver@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0" +"timeline-state-resolver@npm:9.2.0-nightly-release52-20241219-123204-90290cef1.0": + version: 9.2.0-nightly-release52-20241219-123204-90290cef1.0 + resolution: "timeline-state-resolver@npm:9.2.0-nightly-release52-20241219-123204-90290cef1.0" dependencies: "@tv2media/v-connection": ^7.3.4 atem-connection: 3.5.0 @@ -26067,7 +26067,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.3 superfly-timeline: ^9.0.1 threadedclass: ^1.2.2 - timeline-state-resolver-types: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 + timeline-state-resolver-types: 9.2.0-nightly-release52-20241219-123204-90290cef1.0 tslib: ^2.6.3 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.13.1 @@ -26075,7 +26075,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^6.0.4 ws: ^8.18.0 xml-js: ^1.6.11 - checksum: a127cd66d96f06bae3ff16291bc1be4cd1c6589c8843632c489e2432df2b34789adc62db0826d7069bb6ff1b4a8c56e0f37ffe5a17d9a92b8f3533963e0bdb71 + checksum: 845905af05db8f1e01d41910fde680d235aabcff6327f25824cacde4610cd64b9add43e6b4f28da797d963283f4d201a77b94cc3c25b7bf8c55ccfe9bf7c0595 languageName: node linkType: hard From 8f0153e294a4f4842e333cc3c4e2bacbf6c4c236 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Mon, 4 Nov 2024 15:55:03 +0000 Subject: [PATCH 13/15] feat: move next part should respect quickloop bounds --- meteor/server/api/userActions.ts | 4 +- .../src/context/adlibActionContext.ts | 2 +- .../src/context/onSetAsNextContext.ts | 2 +- .../blueprints-integration/src/triggers.ts | 7 ++ packages/corelib/src/worker/studio.ts | 1 + .../blueprints/context/OnSetAsNextContext.ts | 10 ++- .../src/blueprints/context/adlibActions.ts | 10 ++- .../src/playout/model/PlayoutModel.ts | 27 +++++--- .../model/implementation/PlayoutModelImpl.ts | 14 ++-- .../model/services/QuickLoopService.ts | 68 ++++++++++++++++++- .../job-worker/src/playout/moveNextPart.ts | 36 ++++++++-- .../job-worker/src/playout/setNextJobs.ts | 8 ++- packages/meteor-lib/src/api/userActions.ts | 3 +- .../meteor-lib/src/triggers/actionFactory.ts | 9 +-- .../actionSelector/ActionSelector.tsx | 15 ++++ .../ui/Shelf/DashboardActionButtonGroup.tsx | 11 ++- 16 files changed, 193 insertions(+), 34 deletions(-) diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 43dc0a46c6..348380cbf6 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -191,7 +191,8 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, partDelta: number, - segmentDelta: number + segmentDelta: number, + ignoreQuickLoop: boolean | null ) { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, @@ -208,6 +209,7 @@ class ServerUserActionAPI playlistId: rundownPlaylistId, partDelta: partDelta, segmentDelta: segmentDelta, + ignoreQuickLoop: ignoreQuickLoop ?? undefined, } ) } diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index ec1b19a4bb..50c28fcf44 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -31,7 +31,7 @@ export interface IActionExecutionContext // getNextShowStyleConfig(): Readonly<{ [key: string]: ConfigItemValue }> /** Move the next part through the rundown. Can move by either a number of parts, or segments in either direction. */ - moveNextPart(partDelta: number, segmentDelta: number): Promise + moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise /** Set flag to perform take after executing the current action. Returns state of the flag after each call. */ takeAfterExecuteAction(take: boolean): Promise /** Inform core that a take out of the current partinstance should be blocked until the specified time */ diff --git a/packages/blueprints-integration/src/context/onSetAsNextContext.ts b/packages/blueprints-integration/src/context/onSetAsNextContext.ts index da6afe52ae..dd0d7f8c37 100644 --- a/packages/blueprints-integration/src/context/onSetAsNextContext.ts +++ b/packages/blueprints-integration/src/context/onSetAsNextContext.ts @@ -76,5 +76,5 @@ export interface IOnSetAsNextContext extends IShowStyleUserContext, IEventContex * Multiple calls of this inside one call to `onSetAsNext` will replace earlier calls. * @returns Whether a new Part was found using the provided offset */ - moveNextPart(partDelta: number, segmentDelta: number): Promise + moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickLoop?: boolean): Promise } diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index c360fa6567..faca7a2847 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -230,6 +230,13 @@ export interface IMoveNextAction extends ITriggeredActionBase { * @memberof IMoveNextAction */ parts: number + /** + * When moving the next part it should ignore any of the boundaries set by the QuickLoop feature + * + * @type {boolean} + * @memberof IMoveNextAction + */ + ignoreQuickLoop: boolean } export interface ICreateSnapshotForDebugAction extends ITriggeredActionBase { diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 9132ae8ba5..6be6da9470 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -231,6 +231,7 @@ export interface StopPiecesOnSourceLayersProps extends RundownPlayoutPropsBase { export interface MoveNextPartProps extends RundownPlayoutPropsBase { partDelta: number segmentDelta: number + ignoreQuickLoop?: boolean } export type ActivateHoldProps = RundownPlayoutPropsBase export type DeactivateHoldProps = RundownPlayoutPropsBase diff --git a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts index 7de1cf88ac..eec0200251 100644 --- a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts +++ b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts @@ -121,7 +121,7 @@ export class OnSetAsNextContext return this.partAndPieceInstanceService.removePieceInstances('next', pieceInstanceIds) } - async moveNextPart(partDelta: number, segmentDelta: number): Promise { + async moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickLoop?: boolean): Promise { if (typeof partDelta !== 'number') throw new Error('partDelta must be a number') if (typeof segmentDelta !== 'number') throw new Error('segmentDelta must be a number') @@ -132,7 +132,13 @@ export class OnSetAsNextContext } this.pendingMoveNextPart = { - selectedPart: selectNewPartWithOffsets(this.jobContext, this.playoutModel, partDelta, segmentDelta), + selectedPart: selectNewPartWithOffsets( + this.jobContext, + this.playoutModel, + partDelta, + segmentDelta, + ignoreQuickLoop + ), } return !!this.pendingMoveNextPart.selectedPart diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 8ce4882fd8..64000b5994 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -157,8 +157,14 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct return this.partAndPieceInstanceService.queuePart(rawPart, rawPieces) } - async moveNextPart(partDelta: number, segmentDelta: number): Promise { - const selectedPart = selectNewPartWithOffsets(this._context, this._playoutModel, partDelta, segmentDelta) + async moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise { + const selectedPart = selectNewPartWithOffsets( + this._context, + this._playoutModel, + partDelta, + segmentDelta, + ignoreQuickloop + ) if (selectedPart) await setNextPartFromPart(this._context, this._playoutModel, selectedPart, true) } diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index a2177dbbc8..a3d5fc84a4 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -167,6 +167,25 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { */ getRundownIds(): RundownId[] + /** + * Returns any segmentId's that are found between 2 quickloop markers, none will be returned if + * the end is before the start. + * @param start A quickloop marker + * @param end A quickloop marker + */ + getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[] + + /** + * Returns any segmentId's that are found between 2 quickloop markers, none will be returned if + * the end is before the start. + * @param start A quickloop marker + * @param end A quickloop marker + */ + getPartsBetweenQuickLoopMarker( + start: QuickLoopMarker, + end: QuickLoopMarker + ): { parts: PartId[]; segments: SegmentId[] } + /** * Search for a PieceInstance in the RundownPlaylist * @param id Id of the PieceInstance @@ -345,14 +364,6 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa */ setQuickLoopMarker(type: 'start' | 'end', marker: QuickLoopMarker | null): void - /** - * Returns any segmentId's that are found between 2 quickloop markers, none will be returned if - * the end is before the start. - * @param start A quickloop marker - * @param end A quickloop marker - */ - getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[] - calculatePartTimings( fromPartInstance: PlayoutPartInstanceModel | null, toPartInstance: PlayoutPartInstanceModel, diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index f7dd272c45..750d5b749a 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -240,6 +240,16 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { return undefined } + getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[] { + return this.quickLoopService.getSegmentsBetweenMarkers(start, end) + } + getPartsBetweenQuickLoopMarker( + start: QuickLoopMarker, + end: QuickLoopMarker + ): { parts: PartId[]; segments: SegmentId[] } { + return this.quickLoopService.getPartsBetweenMarkers(start, end) + } + #isMultiGatewayMode: boolean | undefined = undefined public get isMultiGatewayMode(): boolean { if (this.#isMultiGatewayMode === undefined) { @@ -830,10 +840,6 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#playlistHasChanged = true } - getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[] { - return this.quickLoopService.getSegmentsBetweenMarkers(start, end) - } - /** Notifications */ async getAllNotifications( diff --git a/packages/job-worker/src/playout/model/services/QuickLoopService.ts b/packages/job-worker/src/playout/model/services/QuickLoopService.ts index b9d252ca7a..c0fcfc8a32 100644 --- a/packages/job-worker/src/playout/model/services/QuickLoopService.ts +++ b/packages/job-worker/src/playout/model/services/QuickLoopService.ts @@ -8,7 +8,7 @@ import { 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' +import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PlayoutPartInstanceModel } from '../PlayoutPartInstanceModel' import { JobContext } from '../../../jobs' @@ -150,6 +150,7 @@ export class QuickLoopService { } getSegmentsBetweenMarkers(startMarker: QuickLoopMarker, endMarker: QuickLoopMarker): SegmentId[] { + // note - this function could be refactored to call getPartsBetweenMarkers instead but it will be less efficient const segments = this.playoutModel.getAllOrderedSegments() const segmentIds: SegmentId[] = [] @@ -201,6 +202,71 @@ export class QuickLoopService { return segmentIds } + getPartsBetweenMarkers( + startMarker: QuickLoopMarker, + endMarker: QuickLoopMarker + ): { parts: PartId[]; segments: SegmentId[] } { + const parts = this.playoutModel.getAllOrderedParts() + const segmentIds: SegmentId[] = [] + const partIds: PartId[] = [] + + let passedStart = false + let seenLastRundown = false + let seenLastSegment = false + + for (const p of parts) { + if ( + !passedStart && + ((startMarker.type === QuickLoopMarkerType.PART && p._id === startMarker.id) || + (startMarker.type === QuickLoopMarkerType.SEGMENT && p.segmentId === startMarker.id) || + (startMarker.type === QuickLoopMarkerType.RUNDOWN && p.rundownId === startMarker.id) || + startMarker.type === QuickLoopMarkerType.PLAYLIST) + ) { + // the start marker is this part, this is the first part in the loop, or this is the first segment that is in the loop + // segments from here on are included in the loop + passedStart = true + } + + if (endMarker.type === QuickLoopMarkerType.RUNDOWN) { + // last rundown needs to be inclusive so we need to break once the rundownId is not equal to segment's rundownId + if (p.rundownId === endMarker.id) { + if (!passedStart) { + // we hit the end before the start so quit now: + break + } + seenLastRundown = true + } else if (seenLastRundown) { + // we have passed the last rundown + break + } + } else if (endMarker.type === QuickLoopMarkerType.SEGMENT) { + // last segment needs to be inclusive so we need to break once the segmentId changes but not before + if (p.segmentId === endMarker.id) { + if (!passedStart) { + // we hit the end before the start so quit now: + break + } + seenLastSegment = true + } else if (seenLastSegment) { + // we have passed the last rundown + break + } + } + + if (passedStart) { + if (segmentIds.slice(-1)[0] !== p.segmentId) segmentIds.push(p.segmentId) + partIds.push(p._id) + } + + if (endMarker.type === QuickLoopMarkerType.PART && p._id === endMarker.id) { + // the endMarker is this part so we can quit now + break + } + } + + return { parts: partIds, segments: segmentIds } + } + private areMarkersFlipped(startPosition: MarkerPosition, endPosition: MarkerPosition) { return compareMarkerPositions(startPosition, endPosition) < 0 } diff --git a/packages/job-worker/src/playout/moveNextPart.ts b/packages/job-worker/src/playout/moveNextPart.ts index ca6a3e4e9c..995f9c495c 100644 --- a/packages/job-worker/src/playout/moveNextPart.ts +++ b/packages/job-worker/src/playout/moveNextPart.ts @@ -11,7 +11,8 @@ export function selectNewPartWithOffsets( _context: JobContext, playoutModel: PlayoutModelReadonly, partDelta: number, - segmentDelta: number + segmentDelta: number, + ignoreQuickLoop = false ): ReadonlyDeep | null { const playlist = playoutModel.playlist @@ -23,8 +24,21 @@ export function selectNewPartWithOffsets( if (!refPart || !refPartInstance) throw new Error(`RundownPlaylist "${playlist._id}" has no next and no current part!`) - const rawSegments = playoutModel.getAllOrderedSegments() - const rawParts = playoutModel.getAllOrderedParts() + let rawSegments = playoutModel.getAllOrderedSegments() + let rawParts = playoutModel.getAllOrderedParts() + let allowWrap = false // whether we should wrap to the first part if the curIndex + delta exceeds the total number of parts + + if (!ignoreQuickLoop && playlist.quickLoop?.start && playlist.quickLoop.end) { + const partsInQuickloop = playoutModel.getPartsBetweenQuickLoopMarker( + playlist.quickLoop.start, + playlist.quickLoop.end + ) + if (partsInQuickloop.parts.includes(refPart._id)) { + rawParts = rawParts.filter((p) => partsInQuickloop.parts.includes(p._id)) + rawSegments = rawSegments.filter((s) => partsInQuickloop.segments.includes(s.segment._id)) + allowWrap = true + } + } if (segmentDelta) { // Ignores horizontalDelta @@ -37,7 +51,14 @@ export function selectNewPartWithOffsets( const refSegmentIndex = considerSegments.findIndex((s) => s.segment._id === refPart.segmentId) if (refSegmentIndex === -1) throw new Error(`Segment "${refPart.segmentId}" not found!`) - const targetSegmentIndex = refSegmentIndex + segmentDelta + let targetSegmentIndex = refSegmentIndex + segmentDelta + if (allowWrap) { + targetSegmentIndex = targetSegmentIndex % considerSegments.length + if (targetSegmentIndex < 0) { + // -1 becomes last segment + targetSegmentIndex = considerSegments.length + targetSegmentIndex + } + } const targetSegment = considerSegments[targetSegmentIndex] if (!targetSegment) return null @@ -64,7 +85,6 @@ export function selectNewPartWithOffsets( } } - // TODO - looping playlists if (selectedPart) { // Switch to that part return selectedPart @@ -88,7 +108,11 @@ export function selectNewPartWithOffsets( } // Get the past we are after - const targetPartIndex = refPartIndex + partDelta + let targetPartIndex = allowWrap ? (refPartIndex + partDelta) % playabaleParts.length : refPartIndex + partDelta + if (allowWrap) { + targetPartIndex = targetPartIndex % playabaleParts.length + if (targetPartIndex < 0) targetPartIndex = playabaleParts.length + targetPartIndex // -1 becomes last part + } let targetPart = playabaleParts[targetPartIndex] if (targetPart && targetPart._id === currentPartInstance?.part._id) { // Cant go to the current part (yet) diff --git a/packages/job-worker/src/playout/setNextJobs.ts b/packages/job-worker/src/playout/setNextJobs.ts index fe297ae313..6a041f3c86 100644 --- a/packages/job-worker/src/playout/setNextJobs.ts +++ b/packages/job-worker/src/playout/setNextJobs.ts @@ -68,7 +68,13 @@ export async function handleMoveNextPart(context: JobContext, data: MoveNextPart } }, async (playoutModel) => { - const selectedPart = selectNewPartWithOffsets(context, playoutModel, data.partDelta, data.segmentDelta) + const selectedPart = selectNewPartWithOffsets( + context, + playoutModel, + data.partDelta, + data.segmentDelta, + data.ignoreQuickLoop + ) if (!selectedPart) return null await setNextPartFromPart(context, playoutModel, selectedPart, true) diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index 01db5ba8fe..5ff3351320 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -58,7 +58,8 @@ export interface NewUserActionAPI { eventTime: Time, rundownPlaylistId: RundownPlaylistId, partDelta: number, - segmentDelta: number + segmentDelta: number, + ignoreQuickLoop?: boolean ): Promise> prepareForBroadcast( userEvent: string, diff --git a/packages/meteor-lib/src/triggers/actionFactory.ts b/packages/meteor-lib/src/triggers/actionFactory.ts index 7796716ffc..99667b8a76 100644 --- a/packages/meteor-lib/src/triggers/actionFactory.ts +++ b/packages/meteor-lib/src/triggers/actionFactory.ts @@ -568,15 +568,16 @@ export function createAction( ) } case PlayoutActions.moveNext: - return createUserActionWithCtx(triggersContext, action, UserAction.MOVE_NEXT, async (e, ts, ctx) => - triggersContext.MeteorCall.userAction.moveNext( + return createUserActionWithCtx(triggersContext, action, UserAction.MOVE_NEXT, async (e, ts, ctx) => { + return triggersContext.MeteorCall.userAction.moveNext( e, ts, ctx.rundownPlaylistId.get(null), action.parts ?? 0, - action.segments ?? 0 + action.segments ?? 0, + action.ignoreQuickLoop ) - ) + }) case PlayoutActions.reloadRundownPlaylistData: if (isActionTriggeredFromUiContext(triggersContext, action)) { return createRundownPlaylistSoftResyncAction(action.filterChain as IGUIContextFilterLink[]) diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index d9f150dcf3..c5bcd12512 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -66,6 +66,9 @@ function getArguments(t: TFunction, action: SomeAction): string[] { if (action.parts) { result.push(t('Parts: {{delta}}', { delta: (action.parts > 0 ? '+' : '') + action.parts })) } + if (action.ignoreQuickLoop) { + result.push(t('Ignore QuickLoop')) + } break case PlayoutActions.reloadRundownPlaylistData: break @@ -318,6 +321,18 @@ function getActionParametersEditor( }) } /> + + { + onChange({ + ...action, + ignoreQuickLoop: newVal, + }) + }} + />
) case PlayoutActions.reloadRundownPlaylistData: diff --git a/packages/webui/src/client/ui/Shelf/DashboardActionButtonGroup.tsx b/packages/webui/src/client/ui/Shelf/DashboardActionButtonGroup.tsx index 8e652677da..cdb12c8f6c 100644 --- a/packages/webui/src/client/ui/Shelf/DashboardActionButtonGroup.tsx +++ b/packages/webui/src/client/ui/Shelf/DashboardActionButtonGroup.tsx @@ -41,11 +41,18 @@ export const DashboardActionButtonGroup = withTranslation()( } } - moveNext = (e: any, horizontalDelta: number, verticalDelta: number) => { + moveNext = (e: any, horizontalDelta: number, verticalDelta: number, ignoreQuickLoop?: boolean) => { const { t } = this.props if (this.props.studioMode) { doUserAction(t, e, UserAction.MOVE_NEXT, (e, ts) => - MeteorCall.userAction.moveNext(e, ts, this.props.playlist._id, horizontalDelta, verticalDelta) + MeteorCall.userAction.moveNext( + e, + ts, + this.props.playlist._id, + horizontalDelta, + verticalDelta, + ignoreQuickLoop + ) ) } } From 4253eea19e7354532b0bf56957b5fc66a3daa121 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 7 Jan 2025 11:25:26 +0100 Subject: [PATCH 14/15] chore(release): 1.51.5 --- meteor/CHANGELOG.md | 7 ++++ meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 8 ++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 8 ++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 8 ++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 8 ++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 20 files changed, 92 insertions(+), 53 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index e4498ae87c..f0e4b48d77 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.51.5](///compare/v1.51.4...v1.51.5) (2025-01-07) + + +### Bug Fixes + +* **job-worker/playout:** Hold mode doesn't work at all a7d6999 + ### [1.51.4](///compare/v1.51.3...v1.51.4) (2024-12-04) diff --git a/meteor/package.json b/meteor/package.json index 91f635f3a0..9ffa5aae53 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.4", + "version": "1.51.5", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index c22865dcff..65328eed31 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/shared-lib": 1.51.5 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.4 - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/blueprints-integration": 1.51.5 + "@sofie-automation/shared-lib": 1.51.5 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.4 - "@sofie-automation/corelib": 1.51.4 - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/blueprints-integration": 1.51.5 + "@sofie-automation/corelib": 1.51.5 + "@sofie-automation/shared-lib": 1.51.5 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index 4105116d47..6402ee17d3 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.51.5](https://github.com/nrkno/sofie-core/compare/v1.51.4...v1.51.5) (2025-01-07) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + ## [1.51.4](https://github.com/nrkno/sofie-core/compare/v1.51.3...v1.51.4) (2024-12-04) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 2af513ab05..6bc28f61b3 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.4", + "version": "1.51.5", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.4", + "@sofie-automation/shared-lib": "1.51.5", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 1be22fa65f..d4030a8f9c 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.4", + "version": "1.51.5", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.4", - "@sofie-automation/shared-lib": "1.51.4", + "@sofie-automation/blueprints-integration": "1.51.5", + "@sofie-automation/shared-lib": "1.51.5", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 28ad6a1d6b..4512f72057 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.4", + "version": "1.51.5", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index bbb939e723..8029fc1d6d 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.4", + "version": "1.51.5", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.4", - "@sofie-automation/corelib": "1.51.4", - "@sofie-automation/shared-lib": "1.51.4", + "@sofie-automation/blueprints-integration": "1.51.5", + "@sofie-automation/corelib": "1.51.5", + "@sofie-automation/shared-lib": "1.51.5", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index 3aee830fb3..819bc12178 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.4", + "version": "1.51.5", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 0cd6b534b4..3c1ddd1d5d 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.4", + "version": "1.51.5", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.4", - "@sofie-automation/corelib": "1.51.4", - "@sofie-automation/server-core-integration": "1.51.4", - "@sofie-automation/shared-lib": "1.51.4", + "@sofie-automation/blueprints-integration": "1.51.5", + "@sofie-automation/corelib": "1.51.5", + "@sofie-automation/server-core-integration": "1.51.5", + "@sofie-automation/shared-lib": "1.51.5", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index 12651308e8..4cac07fa5a 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.51.5](https://github.com/nrkno/sofie-core/compare/v1.51.4...v1.51.5) (2025-01-07) + +**Note:** Version bump only for package mos-gateway + + + + + ## [1.51.4](https://github.com/nrkno/sofie-core/compare/v1.51.3...v1.51.4) (2024-12-04) **Note:** Version bump only for package mos-gateway diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index d8c3461b16..3f6d2e949b 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.4", + "version": "1.51.5", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1", - "@sofie-automation/server-core-integration": "1.51.4", - "@sofie-automation/shared-lib": "1.51.4", + "@sofie-automation/server-core-integration": "1.51.5", + "@sofie-automation/shared-lib": "1.51.5", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 4f9e693cc6..a887c805e0 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.4", + "version": "1.51.5", "license": "MIT", "repository": { "type": "git", diff --git a/packages/package.json b/packages/package.json index b38fcdc147..8ca58b6de3 100644 --- a/packages/package.json +++ b/packages/package.json @@ -65,4 +65,4 @@ }, "name": "packages", "packageManager": "yarn@3.5.0" -} \ No newline at end of file +} diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 0396e96547..2fadf7cf31 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.51.5](https://github.com/nrkno/sofie-core/compare/v1.51.4...v1.51.5) (2025-01-07) + +**Note:** Version bump only for package playout-gateway + + + + + ## [1.51.4](https://github.com/nrkno/sofie-core/compare/v1.51.3...v1.51.4) (2024-12-04) **Note:** Version bump only for package playout-gateway diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 77702d6d29..7d99add74d 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.4", + "version": "1.51.5", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.4", - "@sofie-automation/shared-lib": "1.51.4", + "@sofie-automation/server-core-integration": "1.51.5", + "@sofie-automation/shared-lib": "1.51.5", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.2.1", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index b259defec9..915d072c8f 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.51.5](https://github.com/nrkno/sofie-core/compare/v1.51.4...v1.51.5) (2025-01-07) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + ## [1.51.4](https://github.com/nrkno/sofie-core/compare/v1.51.3...v1.51.4) (2024-12-04) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index e8be09e879..60e0be8345 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.4", + "version": "1.51.5", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.4", + "@sofie-automation/shared-lib": "1.51.5", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 60928b7eaa..50e78219f8 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.4", + "version": "1.51.5", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 2cd2a6209b..0134d3c2ca 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4565,11 +4565,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.4, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.5, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/shared-lib": 1.51.5 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4606,12 +4606,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.4, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.5, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.4 - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/blueprints-integration": 1.51.5 + "@sofie-automation/shared-lib": 1.51.5 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4642,9 +4642,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.4 - "@sofie-automation/corelib": 1.51.4 - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/blueprints-integration": 1.51.5 + "@sofie-automation/corelib": 1.51.5 + "@sofie-automation/shared-lib": 1.51.5 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4674,11 +4674,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.4, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.5, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/shared-lib": 1.51.5 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4688,7 +4688,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.4, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.5, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15334,10 +15334,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.4 - "@sofie-automation/corelib": 1.51.4 - "@sofie-automation/server-core-integration": 1.51.4 - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/blueprints-integration": 1.51.5 + "@sofie-automation/corelib": 1.51.5 + "@sofie-automation/server-core-integration": 1.51.5 + "@sofie-automation/shared-lib": 1.51.5 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17410,8 +17410,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1 - "@sofie-automation/server-core-integration": 1.51.4 - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/server-core-integration": 1.51.5 + "@sofie-automation/shared-lib": 1.51.5 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19397,8 +19397,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.4 - "@sofie-automation/shared-lib": 1.51.4 + "@sofie-automation/server-core-integration": 1.51.5 + "@sofie-automation/shared-lib": 1.51.5 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.2.1 From b87cd9fa611bb84925cf270ed0989578f27f61cb Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Tue, 7 Jan 2025 15:02:15 +0000 Subject: [PATCH 15/15] chore: add defaults --- .../server/migration/upgrades/defaultSystemActionTriggers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meteor/server/migration/upgrades/defaultSystemActionTriggers.ts b/meteor/server/migration/upgrades/defaultSystemActionTriggers.ts index 29242cbecd..2a6bd9b122 100644 --- a/meteor/server/migration/upgrades/defaultSystemActionTriggers.ts +++ b/meteor/server/migration/upgrades/defaultSystemActionTriggers.ts @@ -281,6 +281,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = { ], parts: 1, segments: 0, + ignoreQuickLoop: false, }, }, triggers: { @@ -305,6 +306,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = { ], parts: 0, segments: 1, + ignoreQuickLoop: false, }, }, triggers: { @@ -329,6 +331,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = { ], parts: -1, segments: 0, + ignoreQuickLoop: false, }, }, triggers: { @@ -353,6 +356,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = { ], parts: 0, segments: -1, + ignoreQuickLoop: false, }, }, triggers: {