diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 6ee0cfb659..4e7a8dcb2b 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -265,35 +265,6 @@ jobs: labels: ${{ steps.dockerhub-tag.outputs.labels }} tags: ${{ steps.dockerhub-tag.outputs.tags }} - # Trivy scanning - - name: Get image for Trivy scanning - id: trivy-image - if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - run: | - image=$(echo ${{ steps.ghcr-tag.outputs.tags }} | head -n 1) - echo "image=$image" >> $GITHUB_OUTPUT - - name: Trivy scanning - if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - uses: aquasecurity/trivy-action@0.35.0 - env: - TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db - with: - image-ref: "${{ steps.trivy-image.outputs.image }}" - format: "table" - output: trivy-scan-result.txt - ignore-unfixed: true - severity: "CRITICAL,HIGH" - - name: Post all Trivy scan results to Github Summary as a table - if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - env: - CODE_BLOCK: "```" - run: | - echo "# Trivy scan results ~ core" >> $GITHUB_STEP_SUMMARY - - echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY - cat trivy-scan-result.txt >> $GITHUB_STEP_SUMMARY - echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY - build-gateways: # TODO - should this be dependant on tests or something passing if we are on a tag? name: Build gateways @@ -437,35 +408,6 @@ jobs: labels: ${{ steps.dockerhub-tag.outputs.labels }} tags: "${{ steps.dockerhub-tag.outputs.tags }}" - # Trivy scanning - - name: Get image for Trivy scanning - id: trivy-image - if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - run: | - image=$(echo ${{ steps.ghcr-tag.outputs.tags }} | head -n 1) - echo "image=$image" >> $GITHUB_OUTPUT - - name: Trivy scanning - if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - uses: aquasecurity/trivy-action@0.35.0 - env: - TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db - with: - image-ref: "${{ steps.trivy-image.outputs.image }}" - format: "table" - output: ${{ matrix.gateway-name }}-trivy-scan-result.txt - ignore-unfixed: true - severity: "CRITICAL,HIGH" - - name: Post all Trivy scan results to Github Summary as a table - if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - env: - CODE_BLOCK: "```" - run: | - echo "# Trivy scan results ~ ${{ matrix.gateway-name }}" >> $GITHUB_STEP_SUMMARY - - echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY - cat ${{ matrix.gateway-name }}-trivy-scan-result.txt >> $GITHUB_STEP_SUMMARY - echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY - lint-packages: name: Lint Packages runs-on: ubuntu-latest diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml deleted file mode 100644 index 00426058cb..0000000000 --- a/.github/workflows/trivy.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Scheduled Trivy Scan -on: - workflow_dispatch: - schedule: - - cron: "0 10 * * 1" - -permissions: - contents: read - packages: read - -jobs: - trivy: - if: ${{ github.repository_owner == 'Sofie-Automation' }} - - name: Trivy scan - runs-on: ubuntu-latest - strategy: - matrix: - image: ["server-core", "playout-gateway", "mos-gateway"] - timeout-minutes: 15 - - steps: - - name: Run Trivy vulnerability scanner (json) - uses: aquasecurity/trivy-action@0.35.0 - env: - TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db - with: - image-ref: ghcr.io/sofie-automation/sofie-core-${{ matrix.image }}:latest - format: json - output: "${{ matrix.image }}-trivy-scan-results.json" - - - name: Run Trivy vulnerability scanner (table) - uses: aquasecurity/trivy-action@0.35.0 - env: - TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db - with: - image-ref: ghcr.io/sofie-automation/sofie-core-${{ matrix.image }}:latest - output: "${{ matrix.image }}-trivy-scan-results.txt" - - - name: Post all scan results to Github Summary as a table - env: - CODE_BLOCK: "```" - run: | - echo "# Trivy scan results ~ sofie-core-${{ matrix.image}}:latest" >> $GITHUB_STEP_SUMMARY - - echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY - cat ${{ matrix.image }}-trivy-scan-results.txt >> $GITHUB_STEP_SUMMARY - echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY - - - name: Run Trivy in GitHub SBOM mode and submit results to Dependency Graph - uses: aquasecurity/trivy-action@0.35.0 - env: - TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db - with: - format: "github" - output: "dependency-results-${{ matrix.image }}.sbom.json" - image-ref: ghcr.io/sofie-automation/sofie-core-${{ matrix.image }}:latest - github-pat: ${{ secrets.GITHUB_TOKEN }} - - - name: Create summary of Trivy issues - run: | - summary=$(jq -r '.Results[] | select(.Vulnerabilities) | .Vulnerabilities | group_by(.Severity) | map({Severity: .[0].Severity, Count: length}) | .[] | [.Severity, .Count] | join(": ")' ${{ matrix.image }}-trivy-scan-results.json | awk 'NR > 1 { printf(" | ") } {printf "%s",$0}') - if [ -z "$summary" ] - then - summary="0 Issues" - fi - echo "SUMMARY=$summary" >> $GITHUB_ENV - echo ${{ env.SUMMARY }} - - # - name: Send Slack Notification - # uses: slackapi/slack-github-action@v2.1.0 - # with: - # webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - # webhook-type: incoming-webhook - # payload: | - # text: "Trivy scan results" - # blocks: - # - type: "header" - # text: - # type: "plain_text" - # text: "Trivy scan results for sofie-core-${{ matrix.image }}:latest" - # - type: "section" - # text: - # type: "mrkdwn" - # text: ":thisisfine: ${{ env.SUMMARY }}" - # - type: "section" - # text: - # type: "mrkdwn" - # text: "Read the full scan results on Github" - # accessory: - # type: "button" - # text: - # type: "plain_text" - # text: ":github: Scan results" - # emoji: true - # value: "workflow_run" - # url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - # action_id: "button-action" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 7b66ee3c2a..413df1cdd2 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -4637,24 +4637,25 @@ __metadata: languageName: node linkType: hard -"fast-xml-builder@npm:^1.0.0": - version: 1.1.1 - resolution: "fast-xml-builder@npm:1.1.1" +"fast-xml-builder@npm:^1.1.4": + version: 1.1.4 + resolution: "fast-xml-builder@npm:1.1.4" dependencies: path-expression-matcher: "npm:^1.1.3" - checksum: 10/9e410f3d13d86ff398fdf712a71151c43c599a42d5cab64659172126b9f4f980135afc80c828026211fdfbcee525f2427d587af3190d0c8d03db5ba595bfade5 + checksum: 10/32937866aaf5a90e69d1f4ee6e15e875248d5b5d2afd70277e9e8323074de4980cef24575a591b8e43c29f405d5f12377b3bad3842dc412b0c5c17a3eaee4b6b languageName: node linkType: hard "fast-xml-parser@npm:^5.2.5": - version: 5.4.2 - resolution: "fast-xml-parser@npm:5.4.2" + version: 5.5.8 + resolution: "fast-xml-parser@npm:5.5.8" dependencies: - fast-xml-builder: "npm:^1.0.0" - strnum: "npm:^2.1.2" + fast-xml-builder: "npm:^1.1.4" + path-expression-matcher: "npm:^1.2.0" + strnum: "npm:^2.2.0" bin: fxparser: src/cli/cli.js - checksum: 10/12585d5dd77113411d01cf41818cfecbbaf8f3d9e8448b1c35f50a7eb51205408bc8db27af5733173a77f96f72d7e121d9e675674f71334569157c77845aba39 + checksum: 10/888f9a5d345e65e34b70d394798a1542603a216f06c140a9671d031b80b42c01ef2e68f2a0ceea45e7703fa80549f0e06da710f5a2faafdc910d1b6b354f0fa0 languageName: node linkType: hard @@ -4789,9 +4790,9 @@ __metadata: linkType: hard "flatted@npm:^3.2.9": - version: 3.3.2 - resolution: "flatted@npm:3.3.2" - checksum: 10/ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 + version: 3.4.2 + resolution: "flatted@npm:3.4.2" + checksum: 10/a9e78fe5c2c1fcd98209a015ccee3a6caa953e01729778e83c1fe92e68601a63e1e69cd4e573010ca99eaf585a581b80ccf1018b99283e6cbc2117bcba1e030f languageName: node linkType: hard @@ -8224,10 +8225,10 @@ __metadata: languageName: node linkType: hard -"path-expression-matcher@npm:^1.1.3": - version: 1.1.3 - resolution: "path-expression-matcher@npm:1.1.3" - checksum: 10/9a607d0bf9807cf86b0a29fb4263f0c00285c13bedafb6ad3efc8bc87ae878da2faf657a9138ac918726cb19f147235a0ca695aec3e4ea1ee04641b6520e6c9e +"path-expression-matcher@npm:^1.1.3, path-expression-matcher@npm:^1.2.0": + version: 1.2.0 + resolution: "path-expression-matcher@npm:1.2.0" + checksum: 10/eab23babd9a97d6cf4841a99825c3e990b70b2b29ea6529df9fb6a1f3953befbc68e9e282a373d7a75aff5dc6542d05a09ee2df036ff9bfddf5e1627b769875b languageName: node linkType: hard @@ -9649,10 +9650,10 @@ __metadata: languageName: node linkType: hard -"strnum@npm:^2.1.2": - version: 2.2.0 - resolution: "strnum@npm:2.2.0" - checksum: 10/2969dbc8441f5af1b55db1d2fcea64a8f912de18515b57f85574e66bdb8f30ae76c419cf1390b343d72d687e2aea5aca82390f18b9e0de45d6bcc6d605eb9385 +"strnum@npm:^2.2.0": + version: 2.2.2 + resolution: "strnum@npm:2.2.2" + checksum: 10/c55813cfded750dc84556b4881ffc7cee91382ff15a48f1fba0ff7a678e1640ed96ca40806fbd55724940fd7d51cf752469b2d862e196e4adefb6c7d5d9cd73b languageName: node linkType: hard diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 6f52a1529a..8f40ee2dfa 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -1893,11 +1893,11 @@ describe('Timeline', () => { currentInfinitePieces: { piece002: { // Should still be based on the time of previousPart - partGroup: { start: firstPartTakeTime }, pieceGroup: { controlObj: { start: 500 }, childGroup: { preroll: 0, postroll: 0 }, }, + partGroup: { start: firstPartTakeTime - 500 }, // this is Piece plannedStartedPlayback minus pieceGroup.controlObj.start }, }, previousOutTransition: undefined, @@ -1973,7 +1973,7 @@ describe('Timeline', () => { }, }, partGroup: { - start: plannedStartedPlayback, + start: plannedStartedPlayback - 500, // this is plannedStartedPlayback minus pieceGroup.controlObj.start }, }, }, diff --git a/packages/job-worker/src/playout/timeline/multi-gateway.ts b/packages/job-worker/src/playout/timeline/multi-gateway.ts index b2ee15f78b..f15aff6de7 100644 --- a/packages/job-worker/src/playout/timeline/multi-gateway.ts +++ b/packages/job-worker/src/playout/timeline/multi-gateway.ts @@ -8,6 +8,7 @@ import { PlayoutModel } from '../model/PlayoutModel.js' import { RundownTimelineTimingContext, getInfinitePartGroupId } from './rundown.js' import { PlayoutPartInstanceModel } from '../model/PlayoutPartInstanceModel.js' import { PlayoutPieceInstanceModel } from '../model/PlayoutPieceInstanceModel.js' +import { getPieceControlObjectId } from '@sofie-automation/corelib/dist/playout/ids' /** * We want it to be possible to generate a timeline without it containing any `start: 'now'`. @@ -297,7 +298,7 @@ function setPlannedTimingsOnPieceInstance( const userDurationEnd = pieceInstance.pieceInstance.userDuration && 'endRelativeToPart' in pieceInstance.pieceInstance.userDuration - ? pieceInstance.pieceInstance.userDuration.endRelativeToPart + ? partPlannedStart + pieceInstance.pieceInstance.userDuration.endRelativeToPart : null let plannedEnd: number | undefined = userDurationEnd ?? undefined @@ -334,6 +335,19 @@ function preserveOrTrackInfiniteTimings( // Update the timeline group const startedPlayback = plannedStartedPlayback ?? pieceInstance.pieceInstance.plannedStartedPlayback if (startedPlayback) { + const pieceControlObjectId = getPieceControlObjectId(pieceInstance.pieceInstance) + const pieceControlObj = timelineObjsMap[pieceControlObjectId] + + // this replicates what generateCurrentInfinitePieceObjects() does + let pieceEnableStartOffset = 0 + if ( + pieceControlObj && + !Array.isArray(pieceControlObj.enable) && + typeof pieceControlObj.enable?.start === 'number' + ) { + pieceEnableStartOffset = pieceControlObj.enable.start + } + const infinitePartGroupId = getInfinitePartGroupId(pieceInstance.pieceInstance._id) const infinitePartGroupObj = timelineObjsMap[infinitePartGroupId] if ( @@ -341,7 +355,7 @@ function preserveOrTrackInfiniteTimings( !Array.isArray(infinitePartGroupObj.enable) && typeof infinitePartGroupObj.enable.start === 'string' ) { - infinitePartGroupObj.enable.start = startedPlayback + infinitePartGroupObj.enable.start = startedPlayback - pieceEnableStartOffset } } } diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index d2848356d6..7af782d4ee 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -282,6 +282,16 @@ function generateCurrentInfinitePieceObjects( return [] } + /* + Notes on the "Infinite Part Group": + Infinite pieces are put into a parent "infinite Part Group" object instead of the usual Part Group, + because their lifetime can be outside of their Part. + + The Infinite Part Group's start time is set to be the start time of the Piece, but this is then complicated by + the Piece.enable.start assuming that it is relative to the PartGroup it is in. This is being factored in if an + absolute start time is known for the piece. + */ + const { infiniteGroupEnable, pieceEnable, nowInParent } = calculateInfinitePieceEnable( currentPartInfo, timingContext, @@ -350,7 +360,12 @@ function calculateInfinitePieceEnable( ) let infiniteGroupEnable: PartEnable = { - start: `#${timingContext.currentPartGroup.id}.start`, // This gets overriden with a concrete time if the original piece is known to have already started + /* + This gets overridden with a concrete time if the original piece is known to have already started + but if not, allows the pieceEnable to be relative to the currentPartInstance's part group as normal + and `nowInParent` to be correct for the piece objects inside + */ + start: `#${timingContext.currentPartGroup.id}.start`, } let nowInParent = currentPartInfo.partTimes.nowInPart // Where is 'now' inside of the infiniteGroup? diff --git a/packages/meteor-lib/src/triggers/RundownViewEventBus.ts b/packages/meteor-lib/src/triggers/RundownViewEventBus.ts index 5cb11ce75c..570b779c36 100644 --- a/packages/meteor-lib/src/triggers/RundownViewEventBus.ts +++ b/packages/meteor-lib/src/triggers/RundownViewEventBus.ts @@ -49,6 +49,7 @@ export enum RundownViewEvents { TOGGLE_SHELF_DROPZONE = 'toggleShelfDropzone', ITEM_DROPPED = 'itemDropped', + CLOSE_NOTIFICATIONS = 'closeNotifications', } export interface IEventContext { @@ -162,6 +163,7 @@ export interface RundownViewEventBusEvents { [RundownViewEvents.CREATE_SNAPSHOT_FOR_DEBUG]: [e: BaseEvent] [RundownViewEvents.TOGGLE_SHELF_DROPZONE]: [e: ToggleShelfDropzoneEvent] [RundownViewEvents.ITEM_DROPPED]: [e: ItemDroppedEvent] + [RundownViewEvents.CLOSE_NOTIFICATIONS]: [] } class RundownViewEventBus0 extends EventEmitter {} diff --git a/packages/package.json b/packages/package.json index d6f1019368..15749304d0 100644 --- a/packages/package.json +++ b/packages/package.json @@ -60,8 +60,8 @@ "jest-environment-jsdom": "^30.2.0", "jest-mock-extended": "^4.0.0", "json-schema-to-typescript": "^15.0.4", - "lerna": "^9.0.5", - "nodemon": "^2.0.22", + "lerna": "^9.0.7", + "nodemon": "^3.1.14", "open-cli": "^8.0.0", "pinst": "^3.0.0", "prettier": "^3.8.1", diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index 10dfaa86eb..d56a62f6ed 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -32,6 +32,10 @@ background: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.15) 100%); } + .propertiespanel-pop-up__label-with-icon { + margin-left: 0.5em; + } + .propertiespanel-pop-up { background: #2e2e2e; border-radius: 1px; @@ -64,8 +68,6 @@ letter-spacing: 0.5px; > .svg { - width: 1em; - height: 1.2em; flex-shrink: 0; } > .title { @@ -87,13 +89,16 @@ flex-shrink: 0; } > .propertiespanel-pop-up_close { - height: 1em; - margin-left: 1em; background-color: unset; border: none; } } + .propertiespanel-pop-up__buttons-container { + display: flex; + gap: 0.5em; + } + > .propertiespanel-pop-up__footer { flex: 1; flex: 0 0 0; @@ -109,7 +114,9 @@ > .propertiespanel-pop-up__button, .propertiespanel-pop-up__button-group .propertiespanel-pop-up__button { - display: block; + display: flex; + gap: 0.5em; + align-items: center; border-radius: 5px; border: 1px solid #7f7f7f; @@ -227,13 +234,14 @@ } .propertiespanel-pop-up__button { - // margin-top: 10px; + display: flex; + align-items: center; background: #636363; padding: 10px; - gap: 10px; border-radius: 5px; border: 1px solid #7f7f7f; color: #dfdfdf; + gap: 0.5em; font-size: 0.875em; font-weight: 500; @@ -266,63 +274,6 @@ width: 100%; } - // // Force the base input-l class - // .input-l { - // width: 100% !important; - // max-width: none !important; - // margin-left: 0px; - // border: none; - // } - - // // Force the select/text-input defaults - // .select, - // .inline-select, - // .text-input { - // display: block !important; - // position: relative; - // width: 100% !important; - // } - - // .input { - // border: 1px solid #e5e7eb; - // border-radius: 0.375rem; - // padding: 0.5rem 0.75rem; - // width: 100%; - - // &:focus { - // outline: none; - // border-color: #3b82f6; - // box-shadow: 0 0 0 1px #3b82f6; - // background-color: unset !important; // origo >.> - // } - // } - - // .label-text { - // &:before { - // content: none !important; - // } - // } - - // .dropdown { - // background: white; - // border: 1px solid #e5e7eb; - // border-radius: 0.375rem; - // width: 100%; - // max-width: 100%; - - // .input, - // .input-l { - // border: none; - // outline: none; - // box-shadow: none; - // } - - // &:focus-within { - // border-color: #3b82f6; - // box-shadow: 0 0 0 1px #3b82f6; - // } - // } - .form-switch { margin: 0.5rem 0; @@ -337,13 +288,6 @@ margin-bottom: 0.5rem; // Increased spacing between label and selector margin-top: 0.5rem; // Clearance from the previous } - - // .label { - // font-size: 0.875rem; - // font-weight: 500; - // color: #374151; - // display: block; - // } } } } diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index b647eabfa6..decf558e5f 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -161,7 +161,7 @@ $break-width: 35rem; } &.properties-panel-open { - padding-right: $properties-panel-width; + padding-right: calc(#{$properties-panel-width} - 3.5em); transition: 0s padding-right 1s; > .rundown-header .rundown-overview { @@ -208,9 +208,13 @@ body.no-overflow { left: 0; bottom: 0; right: 0; - - background: - linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), + background: linear-gradient( + -45deg, + $color-status-fatal 33%, + transparent 33%, + transparent 66%, + $color-status-fatal 66% + ), linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%); @@ -1100,8 +1104,7 @@ svg.icon { } .segment-timeline__part { .segment-timeline__part__invalid-cover { - background-image: - repeating-linear-gradient( + background-image: repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 4px, @@ -1383,8 +1386,7 @@ svg.icon { left: 2px; right: 2px; z-index: 3; - background: - repeating-linear-gradient( + background: repeating-linear-gradient( 45deg, var(--invalid-reason-color-opaque) 0, var(--invalid-reason-color-opaque) 5px, @@ -1565,9 +1567,8 @@ svg.icon { bottom: 0; right: 1px; z-index: 10; - pointer-events: all; - background-image: - repeating-linear-gradient( + pointer-events: none; + background-image: repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 5px, diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index a679007484..8f27fe2e3e 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -380,6 +380,7 @@ const RundownViewContent = translateWithTracker { @@ -906,6 +908,13 @@ const RundownViewContent = translateWithTracker { + NotificationCenter.isOpen = false + this.setState({ + isNotificationsCenterOpen: undefined, + }) + } + private onToggleSupportPanel = () => { this.setState({ isSupportPanelOpen: !this.state.isSupportPanelOpen, @@ -1372,6 +1381,8 @@ const RundownViewContent = translateWithTracker {(selectionContext) => { + const isPropertiesPanelOpen = selectionContext.listSelectedElements().length > 0 + return (
- {this.state.isNotificationsCenterOpen && ( + {!isPropertiesPanelOpen && this.state.isNotificationsCenterOpen && ( )} - {!this.state.isNotificationsCenterOpen && + {isPropertiesPanelOpen && + !this.state.isNotificationsCenterOpen && selectionContext.listSelectedElements().length > 0 && (
@@ -1522,7 +1534,10 @@ const RundownViewContent = translateWithTracker selectionContext.clearAndSetSelection(selection)} + onEditProps={(selection) => { + this.setState({ isNotificationsCenterOpen: undefined }) + selectionContext.clearAndSetSelection(selection) + }} studioMode={this.props.userPermissions.studio} enablePlayFromAnywhere={!!studio.settings.enablePlayFromAnywhere} enableQuickLoop={!!studio.settings.enableQuickLoop} diff --git a/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx b/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx index fdbf8b0f75..4c477c4300 100644 --- a/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx +++ b/packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useState } from 'react' +import React, { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { AdLibActionId, PartId, @@ -215,12 +215,21 @@ export function useSelectedElements( const [segment, setSegment] = useState(undefined) const rundownId = piece ? piece.startRundownId : part ? part.rundownId : segment?.rundownId + const lastValidPiece = useRef(undefined) + useEffect(() => { clearPendingChange() // element id changed so any pending change is for an old element const computation = Tracker.nonreactive(() => Tracker.autorun(() => { - const piece = Pieces.findOne(selectedElement?.elementId) + let piece = Pieces.findOne(selectedElement?.elementId) + + if (!piece && lastValidPiece.current && lastValidPiece.current._id === selectedElement?.elementId) { + piece = lastValidPiece.current + } else if (piece) { + lastValidPiece.current = piece + } + const part = UIParts.findOne({ _id: piece?.startPartId ?? selectedElement?.elementId }) const segment = Segments.findOne({ _id: part ? part.segmentId : selectedElement?.elementId }) diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx index 02f020cc93..51df9dba99 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -17,6 +17,7 @@ import { CoreUserEditingDefinition } from '@sofie-automation/corelib/dist/dataMo import * as RundownResolver from '../../lib/RundownResolver.js' import { SelectedElement } from '../RundownView/SelectedElementsContext.js' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance.js' +import { hasUserEditableContent } from '../UserEditOperations/PropertiesPanel.js' interface IProps { onSetNext: (partInstance: DBPartInstance | DBPart | undefined, e: any, offset?: number, take?: boolean) => void @@ -120,6 +121,10 @@ export function SegmentContextMenu({ part?.instance._id !== playlist.nextPartInfo?.partInstanceId && part?.instance._id !== playlist.previousPartInfo?.partInstanceId + const segmentHasEditableContent = hasUserEditableContent(segment) + const partHasEditableContent = hasUserEditableContent(part?.instance.part) + const pieceHasEditableContent = hasUserEditableContent(piece?.instance.piece) + const isPartOrphaned: boolean | undefined = part ? part.instance.orphaned !== undefined : undefined const isPartNext: boolean | undefined = part ? playlist.nextPartInfo?.partInstanceId === part.instance._id : undefined @@ -159,7 +164,7 @@ export function SegmentContextMenu({ isFormEditable={isSegmentEditAble} /> )} - {enableUserEdits && ( + {enableUserEdits && segmentHasEditableContent && ( <>
onEditProps({ type: 'segment', elementId: part.instance.segmentId })}> @@ -170,140 +175,142 @@ export function SegmentContextMenu({
)} - {part && - isPartNext !== undefined && - isPartOrphaned !== undefined && - !part.instance.part.invalid && - timecode !== null && ( - <> - onSetNext(part.instance.part, e)} - disabled={!!part.instance.orphaned || !canSetAsNext} - > - Next`), - }} - > - - {startsAt !== undefined && part && enablePlayFromAnywhere ? ( - <> - - onSetAsNextFromHere( - part.instance, - playlist?.nextPartInfo?.partInstanceId ?? null, - playlist?.currentPartInfo?.partInstanceId ?? null, - e - ) - } - disabled={getIsPlayFromHereDisabled()} - > - Next` - ), - }} - > - - - onSetAsNextFromHere( - part.instance, - playlist?.nextPartInfo?.partInstanceId ?? null, - playlist?.currentPartInfo?.partInstanceId ?? null, - e, - true - ) - } - disabled={getIsPlayFromHereDisabled(true)} - > - - {t(`Play part from ${RundownUtils.formatTimeToShortTime(Math.floor(timecode / 1000) * 1000)}`)} - - - - ) : null} - {enableQuickLoop && !RundownResolver.isLoopLocked(playlist) && ( - <> - {RundownResolver.isQuickLoopStart(part.partId, playlist) ? ( - onSetQuickLoopStart(null, e)}> - {t('Clear QuickLoop Start')} - - ) : ( + {part && isPartNext !== undefined && isPartOrphaned !== undefined && timecode !== null && ( + <> + {!part.instance.part.invalid && ( + <> + onSetNext(part.instance.part, e)} + disabled={!!part.instance.orphaned || !canSetAsNext} + > + Next`), + }} + > + + {startsAt !== undefined && part && enablePlayFromAnywhere ? ( + <> - onSetQuickLoopStart({ type: QuickLoopMarkerType.PART, id: part.instance.part._id }, e) + onSetAsNextFromHere( + part.instance, + playlist?.nextPartInfo?.partInstanceId ?? null, + playlist?.currentPartInfo?.partInstanceId ?? null, + e + ) } - disabled={!!part.instance.orphaned || !canSetAsNext} + disabled={getIsPlayFromHereDisabled()} > - {t('Set as QuickLoop Start')} - - )} - {RundownResolver.isQuickLoopEnd(part.partId, playlist) ? ( - onSetQuickLoopEnd(null, e)}> - {t('Clear QuickLoop End')} + Next` + ), + }} + > - ) : ( - onSetQuickLoopEnd({ type: QuickLoopMarkerType.PART, id: part.instance.part._id }, e) + onSetAsNextFromHere( + part.instance, + playlist?.nextPartInfo?.partInstanceId ?? null, + playlist?.currentPartInfo?.partInstanceId ?? null, + e, + true + ) } - disabled={!!part.instance.orphaned || !canSetAsNext} + disabled={getIsPlayFromHereDisabled(true)} > - {t('Set as QuickLoop End')} + + {t(`Play part from ${RundownUtils.formatTimeToShortTime(Math.floor(timecode / 1000) * 1000)}`)} + - )} - - )} + + ) : null} + + )} + {enableQuickLoop && !RundownResolver.isLoopLocked(playlist) && ( + <> + {RundownResolver.isQuickLoopStart(part.partId, playlist) ? ( + onSetQuickLoopStart(null, e)}> + {t('Clear QuickLoop Start')} + + ) : ( + + onSetQuickLoopStart({ type: QuickLoopMarkerType.PART, id: part.instance.part._id }, e) + } + disabled={!!part.instance.orphaned || !canSetAsNext} + > + {t('Set as QuickLoop Start')} + + )} + {RundownResolver.isQuickLoopEnd(part.partId, playlist) ? ( + onSetQuickLoopEnd(null, e)}> + {t('Clear QuickLoop End')} + + ) : ( + + onSetQuickLoopEnd({ type: QuickLoopMarkerType.PART, id: part.instance.part._id }, e) + } + disabled={!!part.instance.orphaned || !canSetAsNext} + > + {t('Set as QuickLoop End')} + + )} + + )} + + + {piece && piece.instance.piece.userEditOperations && ( + )} - {piece && piece.instance.piece.userEditOperations && ( - - )} - - {enableUserEdits && ( - <> -
+ {enableUserEdits && (segmentHasEditableContent || partHasEditableContent || pieceHasEditableContent) && ( + <> +
+ {segmentHasEditableContent && ( onEditProps({ type: 'segment', elementId: part.instance.segmentId })}> {t('Edit Segment Properties')} + )} + {partHasEditableContent && ( onEditProps({ type: 'part', elementId: part.instance.part._id })}> {t('Edit Part Properties')} - {piece && piece.instance.piece.userEditProperties && ( - onEditProps({ type: 'piece', elementId: piece.instance.piece._id })}> - {t('Edit Piece Properties')} - - )} - - )} - - )} + )} + {pieceHasEditableContent && piece && ( + onEditProps({ type: 'piece', elementId: piece.instance.piece._id })}> + {t('Edit Piece Properties')} + + )} + + )} + + )} ) : null diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 4adc96a346..09187e9d53 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -58,6 +58,7 @@ import * as RundownResolver from '../../lib/RundownResolver.js' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { SelectedElementsContext } from '../RundownView/SelectedElementsContext.js' import { BlueprintAssetIcon } from '../../lib/Components/BlueprintAssetIcon.js' +import { hasUserEditableContent } from '../UserEditOperations/PropertiesPanel.js' interface IProps { id: string @@ -1015,8 +1016,14 @@ export class SegmentTimelineClass extends React.Component { if (this.props.studio.settings.enableUserEdits) { - if (!selectElementContext.isSelected(this.props.segment._id)) { - selectElementContext.clearAndSetSelection({ type: 'segment', elementId: this.props.segment._id }) + const segment = this.props.segment + + const hasEditableContent = hasUserEditableContent(segment) + if (!hasEditableContent) return + + if (!selectElementContext.isSelected(segment._id)) { + RundownViewEventBus.emit(RundownViewEvents.CLOSE_NOTIFICATIONS) + selectElementContext.clearAndSetSelection({ type: 'segment', elementId: segment._id }) } else { selectElementContext.clearSelections() } diff --git a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx index 0de91e5571..2cab0b2fda 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -18,7 +18,10 @@ import { LocalLayerItemRenderer } from './Renderers/LocalLayerItemRenderer.js' import { DEBUG_MODE } from './SegmentTimelineDebugMode.js' import { getElementDocumentOffset, OffsetPosition } from '../../utils/positions.js' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { RundownViewEvents, HighlightEvent } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' +import RundownViewEventBus, { + RundownViewEvents, + HighlightEvent, +} from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' import { pieceUiClassNames } from '../../lib/ui/pieceUiClassNames.js' import { TransitionSourceRenderer } from './Renderers/TransitionSourceRenderer.js' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' @@ -33,6 +36,7 @@ import { PreviewPopUpContext, } from '../PreviewPopUp/PreviewPopUpContext.js' import { useRundownViewEventBusListener } from '../../lib/lib.js' +import { hasUserEditableContent } from '../UserEditOperations/PropertiesPanel.js' const LEFT_RIGHT_ANCHOR_SPACER = 15 const MARGINAL_ANCHORED_WIDTH = 5 @@ -180,12 +184,15 @@ export const SourceLayerItem = (props: Readonly): JSX.Ele e.stopPropagation() if (studio?.settings.enableUserEdits && !studio?.settings.allowPieceDirectPlay) { - const pieceId = piece.instance.piece._id - if (!selectElementContext.isSelected(pieceId)) { - selectElementContext.clearAndSetSelection({ type: 'piece', elementId: pieceId }) - } else { - selectElementContext.clearSelections() - } + const innerPiece = piece.instance.piece + + const hasEditableContent = hasUserEditableContent(innerPiece) + if (!hasEditableContent) return + + const pieceId = innerPiece._id + + RundownViewEventBus.emit(RundownViewEvents.CLOSE_NOTIFICATIONS) + selectElementContext.clearAndSetSelection({ type: 'piece', elementId: pieceId }) } else if (typeof onDoubleClick === 'function') { onDoubleClick(piece, e) } diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index f48ee56222..93f30f8034 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -18,10 +18,15 @@ import { useTranslation } from 'react-i18next' import { useSelectedElements, useSelectedElementsContext } from '../RundownView/SelectedElementsContext.js' import { RundownUtils } from '../../lib/rundown.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { SchemaFormWithState } from '../../lib/forms/SchemaFormWithState.js' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintAssetIcon } from '../../lib/Components/BlueprintAssetIcon.js' +import { ReadonlyDeep } from 'type-fest' +import { + CoreUserEditingDefinition, + CoreUserEditingProperties, +} from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions.js' type PendingChange = DefaultUserOperationEditProperties['payload'] @@ -35,6 +40,21 @@ export function PropertiesPanel(): JSX.Element { const { piece, part, segment, rundownId } = useSelectedElements(selectedElement, () => setPendingChange(undefined)) + const [hadPiece, setHadPiece] = useState(false) + + useEffect(() => { + if (piece) setHadPiece(true) + }, [piece]) + + useEffect(() => { + const pieceChangedId = selectedElement?.type === 'piece' && hadPiece && piece === undefined + + if (pieceChangedId) { + setHadPiece(false) + clearSelections() + } + }, [selectedElement, piece, hadPiece, clearSelections]) + const handleCommitChanges = async (e: React.MouseEvent) => { if (!rundownId || !selectedElement || !pendingChange) return @@ -182,20 +202,18 @@ export function PropertiesPanel(): JSX.Element {
@@ -357,7 +375,7 @@ function ActionList({ const { t } = useTranslation() return ( -
+
{actions.map((action) => (