From 74df69d1feabe3881156c62cb9096478fb48f629 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Mon, 23 Mar 2026 14:54:58 +0100 Subject: [PATCH 1/7] chore: Added definition for a specific background and text color for the new class for the segment-event-identifier styling. --- packages/webui/src/client/styles/_colorScheme.scss | 3 +++ packages/webui/src/client/styles/defaultColors.scss | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packages/webui/src/client/styles/_colorScheme.scss b/packages/webui/src/client/styles/_colorScheme.scss index 0a50e271f1..57fc93d6e8 100644 --- a/packages/webui/src/client/styles/_colorScheme.scss +++ b/packages/webui/src/client/styles/_colorScheme.scss @@ -31,6 +31,9 @@ $general-timecode-color: var(--general-timecode-color); $part-identifier: var(--part-identifier); $part-identifier-text: var(--part-identifier-text); +$segment-event-identifier: var(--segment-event-identifier); +$segment-event-identifier-text: var(--segment-event-identifier-text); + $ui-button-primary: var(--ui-button-primary); $ui-button-primary--hover: var(--ui-button-primary--hover); $ui-button-primary--translucent: var(--ui-button-primary--translucent); diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index ef618d611d..b3017e39d0 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -31,6 +31,10 @@ --part-identifier: #363636; --part-identifier-text: #eee; + + --segment-event-identifier: #363636; + --segment-event-identifier-text: #eee; + --general-timecode-color: #ffff00; --ui-button-primary: #1769ff; From d1bd9c9107e3c7c72def5a30bacd8e3078576426 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Mon, 23 Mar 2026 16:21:38 +0100 Subject: [PATCH 2/7] chore: Refactored and modernized the font styling of the "parts-identifier" shown in the segment header and on the take lines on each part. --- .../webui/src/client/styles/rundownView.scss | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index ccc3e9d686..de26537f8a 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -587,18 +587,38 @@ svg.icon { grid-row: title-identifiers / end; .segment-timeline__part-identifiers__identifier { - font-weight: 300; - text-shadow: none; - margin: 2.5px; - padding: 1px 4px; + margin: 0.1rem; + padding: 0.1rem 0.35rem 0.1rem 0.35rem; background-color: $part-identifier; color: $part-identifier-text; - border-radius: 10px; - font-size: 0.85rem; + border-radius: 99px; + font-family: "Roboto Flex", "Roboto", sans-serif; + font-style: normal; + font-size: 0.9rem; + letter-spacing: 0.03em; + line-height: 100%; + text-shadow: none; + font-variant-numeric: proportional-nums; + font-variation-settings: + 'wdth' 90, + 'wght' 500, + 'opsz' 120, + 'slnt' 0, + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 514, + 'YTUC' 712; &:hover { - background-color: #8c8c8c; + background-color: #6f6f6f; color: #fff; + font-weight: 700; + box-shadow: 0px 0px 4px 0px #fff; } } } @@ -2492,12 +2512,34 @@ svg.icon { .segment-timeline__identifier { z-index: -1; - padding: 0 4px 0 10px; + padding: 0.1rem 0.35rem 0.1rem 0.6rem; box-sizing: border-box; + border-radius: 0 99px 99px 0; + margin: 0rem; + background-color: $part-identifier; - border-radius: 0 8px 8px 0; color: $part-identifier-text; - font-size: 0.85rem; + font-family: "Roboto Flex", "Roboto", sans-serif; + font-style: normal; + font-size: 0.9rem; + letter-spacing: 0.03em; + line-height: 100%; + text-shadow: none; + font-variant-numeric: proportional-nums; + font-variation-settings: + 'wdth' 90, + 'wght' 500, + 'opsz' 120, + 'slnt' 0, + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 514, + 'YTUC' 712; } &.gap { From 158d0193c716c712e7c9283a426b3dc3bbf2177e Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 24 Mar 2026 12:16:06 +0100 Subject: [PATCH 3/7] chore: Made the parts Identifiers vertically aligned to the bottom of the segment header. --- .../webui/src/client/styles/rundownView.scss | 17 +++++++++++------ .../ui/SegmentTimeline/SegmentTimeline.scss | 4 ---- .../ui/SegmentTimeline/SegmentTimeline.tsx | 1 + 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index de26537f8a..10144a173a 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -539,9 +539,8 @@ svg.icon { } .segment-timeline__title { - display: grid; - grid-template-columns: auto; - grid-template-rows: [title-title] min-content [title-notifications] 1fr [title-identifiers] min-content [end]; + display: flex; + flex-direction: column; margin: 0; padding: 0; @@ -562,6 +561,12 @@ svg.icon { grid-row: title / timeline-header; } + .segment-timeline__title__content { + display: flex; + flex-direction: column; + flex-grow: 1; + } + h2 { margin: 0; padding: 0.2em; @@ -571,7 +576,6 @@ svg.icon { hyphens: auto; -webkit-hyphens: auto; -ms-hyphens: auto; - grid-row: title-title / title-notifications; } .segment-timeline__title__notes { @@ -580,13 +584,13 @@ svg.icon { line-height: 1.4em; font-size: 0.75em; font-weight: 400; - grid-row: title-notifications / title-identifiers; } .segment-timeline__part-identifiers { - grid-row: title-identifiers / end; + margin-top: auto; .segment-timeline__part-identifiers__identifier { + // The pill-shaped labels that appear in the title area of a segment, showing and linking to the part name or number margin: 0.1rem; padding: 0.1rem 0.35rem 0.1rem 0.35rem; background-color: $part-identifier; @@ -2511,6 +2515,7 @@ svg.icon { } .segment-timeline__identifier { + // The Parts identifier label on the right side of the part, showing the Part name or number z-index: -1; padding: 0.1rem 0.35rem 0.1rem 0.6rem; box-sizing: border-box; diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss index d49920a8d4..753593d306 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss @@ -29,10 +29,6 @@ $timeline-layer-height: 1em; vertical-align: text-bottom; } .segment-timeline__title__user-edit-states { - position: absolute; - bottom: 0; - left: 0; - right: 0; display: flex; flex-flow: row nowrap; } diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 09187e9d53..75f05ea2b8 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -1014,6 +1014,7 @@ export class SegmentTimelineClass extends React.Component
{ if (this.props.studio.settings.enableUserEdits) { const segment = this.props.segment From 5c100840c27fc4f022f58fb03ae4586bafa8967e Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 24 Mar 2026 12:45:01 +0100 Subject: [PATCH 4/7] chore: Created a separate class for the segment-event-identifier, as well as mock data in the form of the string "Seg. Identifier". --- .../src/client/styles/defaultColors.scss | 4 +-- .../webui/src/client/styles/rundownView.scss | 34 ++++++++++++++++++- .../SegmentStoryboard/SegmentStoryboard.tsx | 1 + .../ui/SegmentTimeline/SegmentTimeline.tsx | 1 + 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index b3017e39d0..52a32e5d46 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -32,8 +32,8 @@ --part-identifier: #363636; --part-identifier-text: #eee; - --segment-event-identifier: #363636; - --segment-event-identifier-text: #eee; + --segment-event-identifier: #ffee00; + --segment-event-identifier-text: #000000; --general-timecode-color: #ffff00; diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index 10144a173a..74f1bc9bd9 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -590,7 +590,7 @@ svg.icon { margin-top: auto; .segment-timeline__part-identifiers__identifier { - // The pill-shaped labels that appear in the title area of a segment, showing and linking to the part name or number + // The pill-shaped labels that appear in the segment header, showing and linking to the part name or number margin: 0.1rem; padding: 0.1rem 0.35rem 0.1rem 0.35rem; background-color: $part-identifier; @@ -626,6 +626,38 @@ svg.icon { } } } + .segment-timeline__segment-event-identifier{ + // The pill-shaped labels that appear in the segment header, showing extra information about the segment. + align-self: flex-start; + display: inline-block; + margin-top: auto; + margin: 0.25rem; + padding: 0.1rem 0.35rem 0.1rem 0.35rem; + background-color: $segment-event-identifier; + color: $segment-event-identifier-text; + border-radius: 99px; + font-family: "Roboto Flex", "Roboto", sans-serif; + font-style: normal; + font-size: 0.9rem; + letter-spacing: 0.03em; + line-height: 100%; + text-shadow: none; + font-variant-numeric: proportional-nums; + font-variation-settings: + 'wdth' 70, + 'wght' 500, + 'opsz' 120, + 'slnt' 0, + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 514, + 'YTUC' 712; + } } &.has-identifiers { diff --git a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx index f92fbd94d9..402b5b5c9c 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx @@ -599,6 +599,7 @@ export const SegmentStoryboard = React.memo( )}
)} +
Seg. Identifier
{identifiers.length > 0 && (
{identifiers.map((ident) => ( diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 75f05ea2b8..87bf871227 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -1070,6 +1070,7 @@ export class SegmentTimelineClass extends React.Component )} +
Seg. Identifier
{identifiers.length > 0 && (
{identifiers.map((ident) => ( From 97842472d74b99ed8b93d8f33d49ea1bac0abd80 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Wed, 25 Mar 2026 10:46:20 +0100 Subject: [PATCH 5/7] chore: Updated the colours of the segment event identifier. --- packages/webui/src/client/styles/defaultColors.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index 52a32e5d46..f41700da36 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -32,8 +32,8 @@ --part-identifier: #363636; --part-identifier-text: #eee; - --segment-event-identifier: #ffee00; - --segment-event-identifier-text: #000000; + --segment-event-identifier: #363636; + --segment-event-identifier-text: #fff; --general-timecode-color: #ffff00; From a98ac5c96672de186249d3dff63eedfd90cbae3b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 25 Mar 2026 13:02:03 +0000 Subject: [PATCH 6/7] chore: ui refactoring --- .../SegmentAdlibTesting.tsx | 39 +--- .../SegmentAdlibTestingContainer.tsx | 1 - .../getReactivePieceNoteCountsForSegment.tsx | 104 ---------- .../SegmentContainer/withResolvedSegment.ts | 11 -- .../ui/SegmentHeader/SegmentHeaderNotes.tsx | 183 ++++++++++++++++++ .../src/client/ui/SegmentList/SegmentList.tsx | 4 +- .../ui/SegmentList/SegmentListContainer.tsx | 1 - .../ui/SegmentList/SegmentListHeader.tsx | 40 +--- .../SegmentStoryboard/SegmentStoryboard.tsx | 41 +--- .../SegmentStoryboardContainer.tsx | 1 - .../ui/SegmentTimeline/SegmentTimeline.tsx | 43 +--- .../SegmentTimelineContainer.tsx | 1 - 12 files changed, 206 insertions(+), 263 deletions(-) delete mode 100644 packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx create mode 100644 packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx diff --git a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx index 6f0204f720..2b4c709064 100644 --- a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx +++ b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx @@ -1,15 +1,8 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { IContextMenuContext } from '../RundownView.js' -import { - IOutputLayerUi, - PartUi, - PieceUi, - SegmentNoteCounts, - SegmentUi, -} from '../SegmentContainer/withResolvedSegment.js' +import { IOutputLayerUi, PartUi, PieceUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' -import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { contextMenuHoldToDisplayTime, useCombinedRefs, useRundownViewEventBusListener } from '../../lib/lib.js' import { useTranslation } from 'react-i18next' import { literal } from '@sofie-automation/corelib/dist/lib' @@ -33,6 +26,7 @@ import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { isLoopRunning } from '../../lib/RundownResolver.js' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' interface IProps { id: string @@ -41,7 +35,6 @@ interface IProps { playlist: DBRundownPlaylist studio: UIStudio parts: Array - segmentNoteCounts: SegmentNoteCounts hasAlreadyPlayed: boolean hasGuestItems: boolean hasRemoteItems: boolean @@ -84,9 +77,6 @@ export const SegmentAdlibTesting = React.memo( const [squishedHover, setSquishedHover] = useState(null) const squishedHoverTimeout = useRef(null) - const criticalNotes = props.segmentNoteCounts.criticial - const warningNotes = props.segmentNoteCounts.warning - const getSegmentContext = () => { const ctx = literal({ segment: props.segment, @@ -465,30 +455,7 @@ export const SegmentAdlibTesting = React.memo( > {t('Adlib Testing')} - {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.ERROR)} - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.WARNING)} - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} +
{Object.values(props.segment.outputLayers) diff --git a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx index e9f5d3db48..162d908a5d 100644 --- a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx +++ b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx @@ -209,7 +209,6 @@ export const SegmentAdlibTestingContainer = withResolvedSegment(function segment={props.segmentui} studio={props.studio} parts={props.parts} - segmentNoteCounts={props.segmentNoteCounts} onItemClick={props.onPieceClick} onItemDoubleClick={props.onPieceDoubleClick} playlist={props.playlist} diff --git a/packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx b/packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx deleted file mode 100644 index e773d80a0c..0000000000 --- a/packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { NoteSeverity } from '@sofie-automation/blueprints-integration' -import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' -import { MongoFieldSpecifierOnes } from '@sofie-automation/corelib/dist/mongo' -import { UISegmentPartNote } from '@sofie-automation/meteor-lib/dist/api/rundownNotifications' -import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { getIgnorePieceContentStatus } from '../../lib/localStorage.js' -import { UIPartInstances, UIPieceContentStatuses, UISegmentPartNotes } from '../Collections.js' -import { SegmentNoteCounts, SegmentUi } from './withResolvedSegment.js' -import { Notifications } from '../../collections/index.js' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' -import { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel/Notifications' -import { UIPieceContentStatus } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' - -export function getReactivePieceNoteCountsForSegment(segment: SegmentUi): SegmentNoteCounts { - const segmentNoteCounts: SegmentNoteCounts = { - criticial: 0, - warning: 0, - } - - const rawNotes = UISegmentPartNotes.find({ segmentId: segment._id }, { fields: { note: 1 } }).fetch() as Pick< - UISegmentPartNote, - 'note' - >[] - for (const note of rawNotes) { - if (note.note.type === NoteSeverity.ERROR) { - segmentNoteCounts.criticial++ - } else if (note.note.type === NoteSeverity.WARNING) { - segmentNoteCounts.warning++ - } - } - - const mediaObjectStatuses = UIPieceContentStatuses.find( - { - rundownId: segment.rundownId, - segmentId: segment._id, - }, - { - fields: literal>({ - _id: 1, - // @ts-expect-error deep property - 'status.status': 1, - }), - } - ).fetch() as Array & { status: Pick }> - - if (!getIgnorePieceContentStatus()) { - for (const obj of mediaObjectStatuses) { - switch (obj.status.status) { - case PieceStatusCode.OK: - case PieceStatusCode.SOURCE_NOT_READY: - case PieceStatusCode.UNKNOWN: - // Ignore - break - case PieceStatusCode.SOURCE_NOT_SET: - segmentNoteCounts.criticial++ - break - case PieceStatusCode.SOURCE_HAS_ISSUES: - case PieceStatusCode.SOURCE_BROKEN: - case PieceStatusCode.SOURCE_MISSING: - case PieceStatusCode.SOURCE_UNKNOWN_STATE: - segmentNoteCounts.warning++ - break - default: - assertNever(obj.status.status) - segmentNoteCounts.warning++ - break - } - } - } - - // Find any relevant notifications - const partInstancesForSegment = UIPartInstances.find( - { segmentId: segment._id, reset: { $ne: true } }, - { - fields: { - _id: 1, - }, - } - ).fetch() as Array> - const rawNotifications = Notifications.find( - { - $or: [ - { 'relatedTo.segmentId': segment._id }, - { - 'relatedTo.partInstanceId': { $in: partInstancesForSegment.map((p) => p._id) }, - }, - ], - }, - { - fields: { - severity: 1, - }, - } - ).fetch() as Array> - for (const notification of rawNotifications) { - if (notification.severity === NoteSeverity.ERROR) { - segmentNoteCounts.criticial++ - } else if (notification.severity === NoteSeverity.WARNING) { - segmentNoteCounts.warning++ - } - } - - return segmentNoteCounts -} diff --git a/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts b/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts index 7a4c2f0ca0..d0a6022fd2 100644 --- a/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts @@ -17,7 +17,6 @@ import { RundownLayoutFilterBase, RundownViewLayout, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { getReactivePieceNoteCountsForSegment } from './getReactivePieceNoteCountsForSegment.js' import { SegmentViewMode } from './SegmentViewModes.js' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' import { AdlibSegmentUi } from '../../lib/shelf.js' @@ -92,15 +91,9 @@ export interface IResolvedSegmentProps { showDurationSourceLayers?: Set } -export interface SegmentNoteCounts { - criticial: number - warning: number -} - export interface ITrackedResolvedSegmentProps { segmentui: SegmentUi | undefined parts: Array - segmentNoteCounts: SegmentNoteCounts hasRemoteItems: boolean hasGuestItems: boolean hasAlreadyPlayed: boolean @@ -124,7 +117,6 @@ export function withResolvedSegment 0; i--) { @@ -275,7 +265,6 @@ export function withResolvedSegment void +} +interface SegmentNoteCounts { + criticalNotes: number + warningNotes: number + headerNotes: ITranslatableMessage[] +} + +export function SegmentHeaderNotes({ classname, segmentId, onHeaderNoteClick }: SegmentHeaderNotesProps): JSX.Element { + const { t } = useTranslation() + + const { criticalNotes, warningNotes, headerNotes } = useTracker( + () => getReactivePieceNoteCountsForSegment(segmentId), + [segmentId], + { criticalNotes: 0, warningNotes: 0, headerNotes: [] } + ) + + return ( + <> + {(criticalNotes > 0 || warningNotes > 0) && ( +
+ {criticalNotes > 0 && ( +
onHeaderNoteClick?.(segmentId, NoteSeverity.ERROR)} + aria-label={t('Critical problems')} + > + +
{criticalNotes}
+
+ )} + {warningNotes > 0 && ( +
onHeaderNoteClick?.(segmentId, NoteSeverity.WARNING)} + aria-label={t('Warnings')} + > + +
{warningNotes}
+
+ )} +
+ )} + + {headerNotes.map((event, index) => ( +
+ {event.key} +
+ ))} + + ) +} + +function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNoteCounts { + const segmentNoteCounts: SegmentNoteCounts = { + criticalNotes: 0, + warningNotes: 0, + headerNotes: [], // TODO - define + } + + const rawNotes = UISegmentPartNotes.find({ segmentId }, { fields: { note: 1 } }).fetch() as Pick< + UISegmentPartNote, + 'note' + >[] + for (const note of rawNotes) { + switch (note.note.type) { + case NoteSeverity.ERROR: + segmentNoteCounts.criticalNotes++ + break + case NoteSeverity.WARNING: + segmentNoteCounts.warningNotes++ + break + case NoteSeverity.INFO: + // Ignore + break + default: + assertNever(note.note.type) + } + } + + const mediaObjectStatuses = UIPieceContentStatuses.find( + { + segmentId, + }, + { + fields: literal>({ + _id: 1, + // @ts-expect-error deep property + 'status.status': 1, + }), + } + ).fetch() as Array & { status: Pick }> + + if (!getIgnorePieceContentStatus()) { + for (const obj of mediaObjectStatuses) { + switch (obj.status.status) { + case PieceStatusCode.OK: + case PieceStatusCode.SOURCE_NOT_READY: + case PieceStatusCode.UNKNOWN: + // Ignore + break + case PieceStatusCode.SOURCE_NOT_SET: + segmentNoteCounts.criticalNotes++ + break + case PieceStatusCode.SOURCE_HAS_ISSUES: + case PieceStatusCode.SOURCE_BROKEN: + case PieceStatusCode.SOURCE_MISSING: + case PieceStatusCode.SOURCE_UNKNOWN_STATE: + segmentNoteCounts.warningNotes++ + break + default: + assertNever(obj.status.status) + segmentNoteCounts.warningNotes++ + break + } + } + } + + // Find any relevant notifications + const partInstancesForSegment = UIPartInstances.find( + { segmentId: segmentId, reset: { $ne: true } }, + { + fields: { + _id: 1, + }, + } + ).fetch() as Array> + const rawNotifications = Notifications.find( + { + $or: [ + { 'relatedTo.segmentId': segmentId }, + { + 'relatedTo.partInstanceId': { $in: partInstancesForSegment.map((p) => p._id) }, + }, + ], + }, + { + fields: { + severity: 1, + message: 1, + }, + } + ).fetch() as Array> + for (const notification of rawNotifications) { + switch (notification.severity) { + case NoteSeverity.ERROR: + segmentNoteCounts.criticalNotes++ + break + case NoteSeverity.WARNING: + segmentNoteCounts.warningNotes++ + break + case NoteSeverity.INFO: + // Ignore + break + default: + assertNever(notification.severity) + } + } + + return segmentNoteCounts +} diff --git a/packages/webui/src/client/ui/SegmentList/SegmentList.tsx b/packages/webui/src/client/ui/SegmentList/SegmentList.tsx index 4f439e1109..f1bc08c0e7 100644 --- a/packages/webui/src/client/ui/SegmentList/SegmentList.tsx +++ b/packages/webui/src/client/ui/SegmentList/SegmentList.tsx @@ -2,7 +2,7 @@ import React, { ReactNode, useLayoutEffect, useMemo, useRef, useState } from 're import classNames from 'classnames' import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { UIStateStorage } from '../../lib/UIStateStorage.js' -import { PartUi, PieceUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' +import { PartUi, PieceUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { IContextMenuContext } from '../RundownView.js' import { useCombinedRefs } from '../../lib/lib.js' import { literal } from '@sofie-automation/corelib/dist/lib' @@ -31,7 +31,6 @@ interface IProps { segment: SegmentUi playlist: DBRundownPlaylist parts: Array - segmentNoteCounts: SegmentNoteCounts fixedSegmentDuration: boolean showCountdownToSegment: boolean @@ -227,7 +226,6 @@ const SegmentListInner = React.forwardRef(function Segme parts={props.parts} segment={props.segment} playlist={props.playlist} - segmentNoteCounts={props.segmentNoteCounts} highlight={highlight} isLiveSegment={props.isLiveSegment} isNextSegment={props.isNextSegment} diff --git a/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx b/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx index 4bd07d4c8e..5fed38f774 100644 --- a/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx +++ b/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx @@ -201,7 +201,6 @@ export const SegmentListContainer = withResolvedSegment(function Segment parts={props.parts} playlist={props.playlist} currentPartWillAutoNext={currentPartWillAutoNext} - segmentNoteCounts={props.segmentNoteCounts} isLiveSegment={isLiveSegment} isNextSegment={isNextSegment} isQueuedSegment={props.playlist.queuedSegmentId === props.segmentui._id} diff --git a/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx b/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx index f8e65fbe38..a40a307b17 100644 --- a/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx +++ b/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx @@ -5,7 +5,7 @@ import { contextMenuHoldToDisplayTime } from '../../lib/lib.js' import { ErrorBoundary } from '../../lib/ErrorBoundary.js' import { SwitchViewModeButton } from '../SegmentContainer/SwitchViewModeButton.js' import { SegmentViewMode } from '../SegmentContainer/SegmentViewModes.js' -import { PartUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' +import { PartUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { PartCountdown } from '../RundownView/RundownTiming/PartCountdown.js' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration.js' import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -13,8 +13,8 @@ import { useTranslation } from 'react-i18next' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IContextMenuContext } from '../RundownView.js' import { NoteSeverity } from '@sofie-automation/blueprints-integration' -import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { SegmentTimeAnchorTime } from '../RundownView/RundownTiming/SegmentTimeAnchorTime.js' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' export function SegmentListHeader({ isDetached, @@ -23,7 +23,6 @@ export function SegmentListHeader({ parts, playlist, highlight, - segmentNoteCounts, isLiveSegment, isNextSegment, isQueuedSegment, @@ -42,7 +41,6 @@ export function SegmentListHeader({ segment: SegmentUi playlist: DBRundownPlaylist parts: Array - segmentNoteCounts: SegmentNoteCounts highlight: boolean isLiveSegment: boolean isNextSegment: boolean @@ -78,9 +76,6 @@ export function SegmentListHeader({ // setDetached(shouldDetach) // } - const criticalNotes = segmentNoteCounts.criticial - const warningNotes = segmentNoteCounts.warning - const contents = ( )}
- {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
onHeaderNoteClick?.(segment._id, NoteSeverity.ERROR)} - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
onHeaderNoteClick?.(segment._id, NoteSeverity.WARNING)} - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} + + + diff --git a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx index 402b5b5c9c..f7fe4c1cdb 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx @@ -2,15 +2,8 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IContextMenuContext } from '../RundownView.js' -import { - IOutputLayerUi, - PartUi, - PieceUi, - SegmentNoteCounts, - SegmentUi, -} from '../SegmentContainer/withResolvedSegment.js' +import { IOutputLayerUi, PartUi, PieceUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' -import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration.js' import { PartCountdown } from '../RundownView/RundownTiming/PartCountdown.js' import { contextMenuHoldToDisplayTime, useCombinedRefs, useRundownViewEventBusListener } from '../../lib/lib.js' @@ -47,6 +40,7 @@ import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/Rundo import { SegmentTimeAnchorTime } from '../RundownView/RundownTiming/SegmentTimeAnchorTime.js' import * as RundownResolver from '../../lib/RundownResolver.js' import { logger } from '../../lib/logging.js' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' interface IProps { id: string @@ -55,7 +49,6 @@ interface IProps { playlist: DBRundownPlaylist studio: UIStudio parts: Array - segmentNoteCounts: SegmentNoteCounts // timeScale: number // maxTimeScale: number // onRecalculateMaxTimeScale: () => Promise @@ -131,9 +124,6 @@ export const SegmentStoryboard = React.memo( } } - const criticalNotes = props.segmentNoteCounts.criticial - const warningNotes = props.segmentNoteCounts.warning - const [useTimeOfDayCountdowns, setUseTimeOfDayCountdowns] = useState( UIStateStorage.getItemBoolean( `rundownView.${props.playlist._id}`, @@ -575,31 +565,8 @@ export const SegmentStoryboard = React.memo( > {props.segment.name} - {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.ERROR)} - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.WARNING)} - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} -
Seg. Identifier
+ + {identifiers.length > 0 && (
{identifiers.map((ident) => ( diff --git a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx index eac9c3a1df..10502da725 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx @@ -211,7 +211,6 @@ export const SegmentStoryboardContainer = withResolvedSegment(function S segment={props.segmentui} studio={props.studio} parts={props.parts} - segmentNoteCounts={props.segmentNoteCounts} onItemClick={props.onPieceClick} onItemDoubleClick={props.onPieceDoubleClick} playlist={props.playlist} diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 87bf871227..04b23a4a03 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -27,7 +27,6 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { contextMenuHoldToDisplayTime } from '../../lib/lib.js' -import { WarningIconSmall, CriticalIconSmall } from '../../lib/ui/icons/notifications.js' import RundownViewEventBus, { RundownViewEvents, HighlightEvent, @@ -44,7 +43,6 @@ import { SwitchViewModeButton } from '../SegmentContainer/SwitchViewModeButton.j import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { SegmentNoteCounts } from '../SegmentContainer/withResolvedSegment.js' import { PartExtended } from '../../lib/RundownResolver.js' import { withTiming, @@ -59,6 +57,7 @@ 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' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' interface IProps { id: string @@ -68,7 +67,6 @@ interface IProps { followLiveSegments: boolean studio: UIStudio parts: Array - segmentNoteCounts: SegmentNoteCounts timeScale: number maxTimeScale: number onRecalculateMaxTimeScale: () => Promise @@ -943,9 +941,6 @@ export class SegmentTimelineClass extends React.Component = this.props.parts .map((p) => p.instance.part.identifier @@ -1040,37 +1035,11 @@ export class SegmentTimelineClass extends React.Component {this.props.segment.name} - {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
- this.props.onHeaderNoteClick && - this.props.onHeaderNoteClick(this.props.segment._id, NoteSeverity.ERROR) - } - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
- this.props.onHeaderNoteClick && - this.props.onHeaderNoteClick(this.props.segment._id, NoteSeverity.WARNING) - } - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} -
Seg. Identifier
+ + {identifiers.length > 0 && (
{identifiers.map((ident) => ( diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index bd520a2696..88ee954cf3 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -664,7 +664,6 @@ const SegmentTimelineContainerContent = withResolvedSegment( segment={this.props.segmentui} studio={this.props.studio} parts={this.props.parts} - segmentNoteCounts={this.props.segmentNoteCounts} timeScale={this.state.timeScale} maxTimeScale={this.state.maxTimeScale} onRecalculateMaxTimeScale={this.updateMaxTimeScale} From 65fca0de5110ebd7d5e3c8c54a5848baa7f204b1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 31 Mar 2026 14:23:43 +0100 Subject: [PATCH 7/7] feat: use segmentHeaderNotes --- .../src/documents/part.ts | 3 ++ .../job-worker/src/blueprints/context/lib.ts | 11 ++++- .../src/ingest/generationSegment.ts | 3 ++ .../ui/SegmentHeader/SegmentHeaderNotes.tsx | 45 +++++++++++++++++-- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/blueprints-integration/src/documents/part.ts b/packages/blueprints-integration/src/documents/part.ts index 42283ac360..cf778b25c7 100644 --- a/packages/blueprints-integration/src/documents/part.ts +++ b/packages/blueprints-integration/src/documents/part.ts @@ -91,6 +91,9 @@ export interface IBlueprintMutatablePart): IBlueprintP displayDurationGroup: part.displayDurationGroup, displayDuration: part.displayDuration, identifier: part.identifier, + segmentHeaderNotes: clone(part.segmentHeaderNotes), hackListenToMediaObjectUpdates: clone( part.hackListenToMediaObjectUpdates ), @@ -705,7 +708,13 @@ export function convertPartialBlueprintMutablePartToCore( blueprintId, ]) } else { - delete playoutUpdatePart.userEditOperations + delete playoutUpdatePart.userEditProperties + } + + if ('segmentHeaderNotes' in updatePart) { + playoutUpdatePart.segmentHeaderNotes = updatePart.segmentHeaderNotes?.map((note) => + wrapTranslatableMessageFromBlueprints(note, [blueprintId]) + ) } return playoutUpdatePart diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index e3583e8515..f563b6db1d 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -377,6 +377,9 @@ function updateModelWithGeneratedPart( ]), } : undefined, + segmentHeaderNotes: blueprintPart.part.segmentHeaderNotes?.map((note) => + wrapTranslatableMessageFromBlueprints(note, [blueprintId]) + ), userEditOperations: translateUserEditsFromBlueprint(blueprintPart.part.userEditOperations, [blueprintId]), userEditProperties: translateUserEditPropertiesFromBlueprint(blueprintPart.part.userEditProperties, [ blueprintId, diff --git a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx index 7672455aaf..a0b5c20448 100644 --- a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx +++ b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx @@ -12,9 +12,10 @@ import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/Part import { literal } from 'shuttle-webhid' import { Notifications } from '../../collections' import { getIgnorePieceContentStatus } from '../../lib/localStorage' -import { UISegmentPartNotes, UIPieceContentStatuses, UIPartInstances } from '../Collections' +import { UISegmentPartNotes, UIPieceContentStatuses, UIPartInstances, UIParts } from '../Collections' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import type { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' export interface SegmentHeaderNotesProps { /** Override the classname of the root div */ @@ -77,7 +78,7 @@ function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNote const segmentNoteCounts: SegmentNoteCounts = { criticalNotes: 0, warningNotes: 0, - headerNotes: [], // TODO - define + headerNotes: [], } const rawNotes = UISegmentPartNotes.find({ segmentId }, { fields: { note: 1 } }).fetch() as Pick< @@ -144,9 +145,15 @@ function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNote { fields: { _id: 1, + // @ts-expect-error deep property + 'part._id': 1, + 'part._rank': 1, + 'part.segmentHeaderNotes': 1, }, } - ).fetch() as Array> + ).fetch() as Array< + Pick & { part: Pick } + > const rawNotifications = Notifications.find( { $or: [ @@ -179,5 +186,37 @@ function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNote } } + const partsForSegment = UIParts.find( + { segmentId }, + { + fields: { + _id: 1, + _rank: 1, + segmentHeaderNotes: 1, + }, + } + ).fetch() as Array> + + // Collect the segment header notes from the parts in part rank order, with partinstance taking priority over the part + const partIdsWithInstance = new Set(partInstancesForSegment.map((pi) => pi.part._id)) + + const mergedNoteEntries: Array<{ rank: number; notes: ITranslatableMessage[] }> = [] + for (const partInstance of partInstancesForSegment) { + if (partInstance.part.segmentHeaderNotes?.length) { + mergedNoteEntries.push({ rank: partInstance.part._rank, notes: partInstance.part.segmentHeaderNotes }) + } + } + for (const part of partsForSegment) { + if (partIdsWithInstance.has(part._id)) continue + if (part.segmentHeaderNotes?.length) { + mergedNoteEntries.push({ rank: part._rank, notes: part.segmentHeaderNotes }) + } + } + + mergedNoteEntries.sort((a, b) => a.rank - b.rank) + for (const entry of mergedNoteEntries) { + segmentNoteCounts.headerNotes.push(...entry.notes) + } + return segmentNoteCounts }