Skip to content

feat: Show AB Resolver Channels on directors screen#1566

Merged
rjmunro merged 16 commits intorelease53from
rjmunro/directors-view-show-ab
Jan 16, 2026
Merged

feat: Show AB Resolver Channels on directors screen#1566
rjmunro merged 16 commits intorelease53from
rjmunro/directors-view-show-ab

Conversation

@rjmunro
Copy link
Contributor

@rjmunro rjmunro commented Nov 27, 2025

About the Contributor

This pull request is posted on behalf of the BBC.

Type of Contribution

This is a Feature

Current Behavior

If you are using Sofie's AB playback features to allocate clips to players, it is hard to know which player will play the next clip. This can be important, e.g. if the sound is being mixed manually.

New Behavior

Icons are now available on the directors screen indicating which player will be used for a clip. What kind of clips and when can be set in the configuration.

Testing

  • I have added one or more unit tests for this PR
  • I have updated the relevant unit tests
  • No unit test changes are needed for this PR

Affected areas

This PR affects directors view and adds a settings screen. It also makes extra controls available to blueprints.

Time Frame

Other Information

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

Summary

This feature PR adds AB Resolver channel indicators to the Directors screen, enabling users to see which playback player (channel) will be used for a clip. The display is fully configurable through both ShowStyle settings UI and blueprint definitions.

Changes by Category

Data Model & Core Types

  • Extended DBShowStyleBase with two new optional configuration objects:
    • abChannelDisplay: user-configurable AB display settings (persisted in database)
    • blueprintAbChannelDisplay: blueprint default values (stored during blueprint upgrade)
  • Both configurations include filters for source layer IDs, source layer types, output layer IDs, and a showOnDirectorScreen flag
  • Added displayAbChannel optional boolean property to IBlueprintPiece to allow per-piece display override
  • Updated type definitions in UIShowStyleBase and related interfaces to include the new abChannelDisplay field

Blueprint Integration & Configuration

  • Extended BlueprintResultApplyShowStyleConfig with optional abChannelDisplay configuration object
  • Enabled blueprints to define and override AB channel display settings per show style
  • Updated convertPieceToBlueprints to propagate the displayAbChannel field from piece instances to blueprint representation

Server & Data Migration

  • Modified showStyleBase.ts migration to:
    • Accumulate fields in an updateSet object for cleaner update payload construction
    • Store optional blueprintAbChannelDisplay from blueprint results
    • Initialize abChannelDisplay from blueprint defaults if not already set
    • Fetch and return abChannelDisplay in relevant projections
  • Updated showStyleUI.ts publication to:
    • Add abChannelDisplay to the ShowStyleBaseFields type
    • Include abChannelDisplay in Mongo projection and returned UI object
  • Updated database mocks to include abChannelDisplay field initialization

Directors Screen Display

  • Implemented AB channel resolution logic in DirectorScreen.tsx:
    • Added shouldDisplayAbChannel() helper function to determine display eligibility based on piece override, show style config, and source/output layer filters
    • Preserved AB session data reactivity by including assignedAbSessions/trackedAbSessions in playlist data fetches
    • Added currentShowStyleBase prop to DirectorScreenRender for AB channel resolution context
    • Integrated useTracker-based computation to determine current and next clip players via AB session pool traversal
    • Added UI elements to render AB channel assignment (single alphanumeric character) or server ID, conditionally based on resolution
    • Refactored conditional rendering for next part visibility and icons to reflect AB channel state

Settings UI

  • Added new "AB Channel Display" settings page with route /ab-channel-display in show style settings menu
  • Implemented AbChannelDisplaySettings component providing:
    • Reset-to-blueprint button (when blueprint defaults exist and overrides are present)
    • Toggle for enabling AB channel display on Directors screen
    • Grouped checkbox interface filtering by source layer type
    • Individual source layer selection checkboxes with indentation
    • Separate list for output layer toggles
    • Persistence via ShowStyleBases.update with $set and $unset operations
  • Added accompanying SCSS styles for header layout, sections, checkboxes, and margins

Reusable Components

  • Created new ColumnPackedGrid component for flexible grid layout:
    • Supports grouped items (with group-level toggles) and standalone items
    • Implements greedy bin-packing algorithm to distribute items across configurable columns (default 3)
    • Computes column heights via provided callback for balanced layout
    • Responsive breakpoints: 1 column (≤768px), 2 columns (769–1200px), 3 columns (1201–1599px), 4 columns (≥1600px)
    • Includes empty state customization and memoized column computation
    • Exports ColumnPackedGridGroup, ColumnPackedGridItem<TItem>, and ColumnPackedGridProps<TItem> interfaces

Coverage Notes

Codecov reports 55.88% patch coverage with 15 lines missing coverage in two files:

  • meteor/server/publications/showStyleUI.ts (0% coverage)
  • meteor/server/migration/upgrades/showStyleBase.ts (73.91% coverage)

Status

Feature implementation is complete with local testing reported. Unit tests and documentation updates remain pending.

@codecov-commenter
Copy link

codecov-commenter commented Nov 27, 2025

Codecov Report

❌ Patch coverage is 31.57895% with 52 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/webui/src/client/lib/rundown.ts 11.90% 37 Missing ⚠️
meteor/server/publications/showStyleUI.ts 0.00% 9 Missing ⚠️
meteor/server/migration/upgrades/showStyleBase.ts 73.91% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

@rjmunro rjmunro changed the base branch from main to release53 November 27, 2025 17:32
@rjmunro rjmunro force-pushed the rjmunro/directors-view-show-ab branch 5 times, most recently from e3778a9 to 94bda04 Compare December 8, 2025 15:34
@rjmunro rjmunro force-pushed the rjmunro/directors-view-show-ab branch from 3e56076 to 94bda04 Compare December 9, 2025 11:11
@Saftret Saftret added the Contribution from BBC Contributions sponsored by BBC (bbc.co.uk) label Dec 10, 2025
@Saftret Saftret marked this pull request as ready for review December 11, 2025 10:56
@Saftret Saftret requested a review from a team as a code owner December 11, 2025 10:56
@Saftret Saftret requested review from Copilot and removed request for Copilot December 11, 2025 10:56
@rjmunro rjmunro force-pushed the rjmunro/directors-view-show-ab branch 3 times, most recently from 8a370d3 to f46cd9d Compare December 11, 2025 14:00
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)
3.8% Duplication on New Code (required ≤ 3%)
D Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@rjmunro rjmunro force-pushed the rjmunro/directors-view-show-ab branch from f46cd9d to 4a00709 Compare January 13, 2026 10:06
@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The PR introduces AB Channel Display configuration support, allowing blueprints to define which source and output layers display AB resolver channel assignments. Changes span data models, blueprint APIs, mock helpers, UI publications, and UI components including a new Director screen integration.

Changes

Cohort / File(s) Summary
Data Model Extensions
packages/corelib/src/dataModel/ShowStyleBase.ts
Added optional abChannelDisplay and blueprintAbChannelDisplay configuration objects with sourceLayerIds, sourceLayerTypes, outputLayerIds, and showOnDirectorScreen fields for AB resolver display control
Blueprint & Integration APIs
packages/blueprints-integration/src/api/showStyle.ts, packages/blueprints-integration/src/documents/piece.ts
Extended BlueprintResultApplyShowStyleConfig with optional abChannelDisplay object; added optional displayAbChannel boolean to IBlueprintPiece for piece-level overrides
UI Layer Type Definitions
packages/meteor-lib/src/api/showStyles.ts
Added optional abChannelDisplay property to UIShowStyleBase
Job Worker Context
packages/job-worker/src/blueprints/context/lib.ts
Added displayAbChannel to blueprint piece sample keys and propagated it through piece conversion to IBlueprintPieceDB
Mock/Test Helpers
meteor/__mocks__/helpers/database.ts, packages/webui/src/__mocks__/helpers/database.ts
Extended mock ShowStyleBase and UIShowStyleBase objects to include abChannelDisplay field
Server Publications & Migrations
meteor/server/publications/showStyleUI.ts, meteor/server/migration/upgrades/showStyleBase.ts
Extended ShowStyleBaseFields type and Mongo projection to include abChannelDisplay; updated migration to handle optional blueprint-provided abChannelDisplay with storage of blueprint defaults
Reusable Grid Component
packages/webui/src/client/ui/Settings/components/ColumnPackedGrid.tsx, packages/webui/src/client/ui/Settings/components/ColumnPackedGrid.scss
Introduced new generic ColumnPackedGrid component with bin-packing algorithm for columnar layout of grouped/standalone items with multi-level checkbox selection and responsive breakpoints
AB Channel Display Settings UI
packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.tsx, packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.scss
Created AbChannelDisplaySettings component for configuring AB channel display per show style, including reset-to-blueprint, director screen toggle, source/output layer filtering with group and item-level checkboxes; uses ColumnPackedGrid for layout
Director Screen Integration
packages/webui/src/client/ui/ClockView/DirectorScreen.tsx
Introduced AB channel display logic with shouldDisplayAbChannel helper; extended reactive data flow to preserve AB session data; added currentShowStyleBase prop; computed currentClipPlayer and nextClipPlayer using AB resolution; updated UI to render AB channel or server ID; simplified conditionals with precomputed countdown
Settings Navigation
packages/webui/src/client/ui/Settings/SettingsMenu.tsx, packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx
Added "AB Channel Display" menu item to show style settings and wired route to AbChannelDisplaySettings component

Sequence Diagram

sequenceDiagram
    participant Blueprint as Blueprint
    participant ShowStyle as ShowStyleBase
    participant Director as DirectorScreen
    participant Session as ABSession Pool

    Blueprint->>ShowStyle: Provides abChannelDisplay config<br/>(sourceLayerIds, outputLayerIds, types)
    ShowStyle->>ShowStyle: Store abChannelDisplay &<br/>blueprintAbChannelDisplay
    
    Director->>ShowStyle: Fetch currentShowStyleBase<br/>with abChannelDisplay config
    Director->>Director: For each PieceInstance:<br/>evaluate shouldDisplayAbChannel()
    
    Director->>Session: Query AB session assignments<br/>for assigned piece
    Session-->>Director: Return ABSession ID if matched
    
    alt AB Channel Display Enabled
        Director->>Director: Render AB Channel ID<br/>(single alphanumeric)
    else AB Channel Display Disabled
        Director->>Director: Render Server Name
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A hop through channels bright and clear,
AB displays that engineers cheer,
New grids pack columns with care,
Settings bloom with filters everywhere,
Director screens show where scenes dare!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Show AB Resolver Channels on directors screen' directly and clearly describes the main feature being added—displaying AB Resolver channel information on the directors screen—which is the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rjmunro rjmunro changed the title Show AB Resolver Channels on directors screen Feat: Show AB Resolver Channels on directors screen Jan 13, 2026
@rjmunro rjmunro changed the title Feat: Show AB Resolver Channels on directors screen feat: Show AB Resolver Channels on directors screen Jan 13, 2026
@jstarpl
Copy link
Contributor

jstarpl commented Jan 13, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @packages/webui/src/client/ui/ClockView/DirectorScreen.tsx:
- Around line 446-505: The two useTracker hooks (for currentClipPlayer and
nextClipPlayer) are being invoked conditionally inside the
playlist/playlistId/segments guard which violates React's Rules of Hooks; move
the useTracker(...) calls out of that conditional so they run unconditionally on
every render (keep the existing early-return checks inside each hook callback
that return undefined when currentPartInstance/nextPartInstance, showStyle data,
or playlist?.assignedAbSessions are missing), ensuring the hooks remain in the
same order and then use the resulting currentClipPlayer and nextClipPlayer
values within the original conditional block.
- Around line 104-107: Remove the debug console.log in the conditional inside
DirectorScreen (the block checking hasSourceLayerIdFilter and
hasSourceLayerTypeFilter): delete the console.log(`[AB Channel] ✓ No source
layer filters specified, showing all`) line and leave the return true; intact so
the behavior is unchanged but no debug output goes to production logs.

In @packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.tsx:
- Around line 59-69: The useMemo in AbChannelDisplay mutates props/state by
calling sort() directly on arrays from showStyleBase.abChannelDisplay and
blueprintDefault; fix it by comparing sorted copies instead (e.g., spread the
arrays into new arrays before sorting) for each of sourceLayerIds,
sourceLayerTypes and outputLayerIds, and guard with defaults like empty arrays
or optional chaining to avoid calling sort on undefined; keep the same boolean
comparisons and the existing dependency array for useMemo.
🧹 Nitpick comments (8)
packages/webui/src/client/ui/Settings/components/ColumnPackedGrid.scss (2)

8-29: Potential conflict between auto-fit grid and explicit media query overrides.

Line 10 defines grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) which provides automatic column fitting. However, the media queries at lines 14-28 explicitly override this with fixed column counts. The auto-fit rule will never apply because the media queries cover all viewport widths.

Consider either removing the auto-fit base rule (since media queries fully override it) or removing the media queries to let auto-fit handle responsiveness naturally.


98-104: Consider refactoring to avoid !important declarations.

The !important declarations may cause specificity issues as the codebase grows. Since the selected state should override hover, consider increasing specificity naturally:

♻️ Suggested refactor
-.column-packed-grid__item--selected {
-	background-color: rgba(76, 175, 80, 0.3) !important;
-
-	&:hover {
-		background-color: rgba(76, 175, 80, 0.4) !important;
-	}
-}
+.column-packed-grid__item.column-packed-grid__item--selected {
+	background-color: rgba(76, 175, 80, 0.3);
+
+	&:hover {
+		background-color: rgba(76, 175, 80, 0.4);
+	}
+}
packages/webui/src/client/ui/Settings/components/ColumnPackedGrid.tsx (2)

153-156: Redundant dependencies in useMemo.

hasGroups and hasItems are derived from groups and items respectively. Including them as dependencies alongside groups and items is redundant and can cause unnecessary recalculations when these derived booleans are recalculated.

♻️ Suggested fix
 const columns = useMemo(() => {
-	if (!hasGroups && !hasItems) return []
+	if ((!groups || groups.length === 0) && (!items || items.length === 0)) return []
 	return packIntoColumns(groups, items, getItemHeight, targetColumns)
-}, [groups, items, getItemHeight, targetColumns, hasGroups, hasItems])
+}, [groups, items, getItemHeight, targetColumns])

71-73: Consider extracting magic numbers as configurable props or constants.

The height constants (GROUP_HEADER_HEIGHT, ITEM_HEIGHT_BASE, GROUP_SPACING) are hardcoded inside the function. If these need to match CSS values elsewhere, consider exporting them as constants or making them configurable via props for better maintainability.

packages/webui/src/client/ui/Settings/SettingsMenu.tsx (1)

354-370: Note: t function missing from useMemo dependencies (pre-existing pattern).

The t function is used inside the useMemo but not listed in dependencies. This follows the existing pattern in this file (e.g., line 285) and typically works because t is stable, but could theoretically cause stale translations if the language changes at runtime.

packages/corelib/src/dataModel/ShowStyleBase.ts (1)

51-70: Consider extracting a shared type for AB Channel Display configuration.

Both abChannelDisplay and blueprintAbChannelDisplay have identical structures. Extracting a shared type would improve maintainability and reduce the risk of the two definitions drifting apart.

♻️ Suggested refactor
+/** Configuration for AB resolver channel display across screens */
+export interface AbChannelDisplayConfig {
+	/** Source layer IDs that should show AB channel info */
+	sourceLayerIds: string[]
+	/** Configure by source layer type */
+	sourceLayerTypes: SourceLayerType[]
+	/** Only show for specific output layers (e.g., only PGM) */
+	outputLayerIds: string[]
+	/** Enable display on Director screen */
+	showOnDirectorScreen: boolean
+	// Future: showOnPresenterScreen, showOnCameraScreen when those views are implemented
+}
+
 export interface DBShowStyleBase {
 	// ... existing fields ...
 
 	/** Configuration for displaying AB resolver channel assignments across different screens */
-	abChannelDisplay?: {
-		/** Source layer IDs that should show AB channel info */
-		sourceLayerIds: string[]
-		/** Configure by source layer type */
-		sourceLayerTypes: SourceLayerType[]
-		/** Only show for specific output layers (e.g., only PGM) */
-		outputLayerIds: string[]
-		/** Enable display on Director screen */
-		showOnDirectorScreen: boolean
-		// Future: showOnPresenterScreen, showOnCameraScreen when those views are implemented
-	}
+	abChannelDisplay?: AbChannelDisplayConfig
 
 	/** Blueprint default for abChannelDisplay (saved during blueprint upgrade) */
-	blueprintAbChannelDisplay?: {
-		sourceLayerIds: string[]
-		sourceLayerTypes: SourceLayerType[]
-		outputLayerIds: string[]
-		showOnDirectorScreen: boolean
-	}
+	blueprintAbChannelDisplay?: AbChannelDisplayConfig
packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.tsx (1)

30-47: Consider internationalizing source layer type labels.

The sourceLayerTypeLabels object uses hardcoded English strings. For consistency with the rest of the UI (which uses t() for translations), these labels should be internationalized.

Suggested approach
 const sourceLayerTypeLabels = useMemo<Partial<Record<SourceLayerType, string>>>(
   () => ({
-    [SourceLayerType.UNKNOWN]: 'Unknown',
-    [SourceLayerType.CAMERA]: 'Camera',
-    [SourceLayerType.VT]: 'VT',
-    [SourceLayerType.REMOTE]: 'Remote',
-    [SourceLayerType.SCRIPT]: 'Script',
-    [SourceLayerType.GRAPHICS]: 'Graphics',
-    [SourceLayerType.SPLITS]: 'Splits',
-    [SourceLayerType.AUDIO]: 'Audio',
-    [SourceLayerType.LOWER_THIRD]: 'Lower Third',
-    [SourceLayerType.LIVE_SPEAK]: 'Live Speak',
-    [SourceLayerType.TRANSITION]: 'Transition',
-    [SourceLayerType.LIGHTS]: 'Lights',
-    [SourceLayerType.LOCAL]: 'Local',
+    [SourceLayerType.UNKNOWN]: t('Unknown'),
+    [SourceLayerType.CAMERA]: t('Camera'),
+    [SourceLayerType.VT]: t('VT'),
+    [SourceLayerType.REMOTE]: t('Remote'),
+    [SourceLayerType.SCRIPT]: t('Script'),
+    [SourceLayerType.GRAPHICS]: t('Graphics'),
+    [SourceLayerType.SPLITS]: t('Splits'),
+    [SourceLayerType.AUDIO]: t('Audio'),
+    [SourceLayerType.LOWER_THIRD]: t('Lower Third'),
+    [SourceLayerType.LIVE_SPEAK]: t('Live Speak'),
+    [SourceLayerType.TRANSITION]: t('Transition'),
+    [SourceLayerType.LIGHTS]: t('Lights'),
+    [SourceLayerType.LOCAL]: t('Local'),
   }),
-  []
+  [t]
 )
meteor/server/migration/upgrades/showStyleBase.ts (1)

132-154: Consider adding type safety to updateSet.

The Record<string, any> type loses the type safety that was previously present when the object was inline. While this works, a more type-safe approach could prevent runtime errors.

Optional improvement
-const updateSet: Record<string, any> = {
+const updateSet: Partial<{
+  'sourceLayersWithOverrides.defaults': ReturnType<typeof normalizeArray>
+  'outputLayersWithOverrides.defaults': ReturnType<typeof normalizeArray>
+  lastBlueprintConfig: DBShowStyleBase['lastBlueprintConfig']
+  blueprintAbChannelDisplay: DBShowStyleBase['blueprintAbChannelDisplay']
+  abChannelDisplay: DBShowStyleBase['abChannelDisplay']
+}> = {

Alternatively, keep the current approach since Mongo update operations typically use dynamic keys.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bae757 and 4a00709.

⛔ Files ignored due to path filters (36)
  • packages/webui/public/icons/channels/0.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/1.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/2.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/3.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/4.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/5.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/6.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/7.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/8.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/9.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/A.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/B.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/C.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/D.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/E.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/F.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/G.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/H.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/I.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/J.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/K.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/L.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/M.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/N.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/O.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/P.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/Q.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/R.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/S.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/T.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/U.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/V.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/W.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/X.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/Y.svg is excluded by !**/*.svg
  • packages/webui/public/icons/channels/Z.svg is excluded by !**/*.svg
📒 Files selected for processing (16)
  • meteor/__mocks__/helpers/database.ts
  • meteor/server/migration/upgrades/showStyleBase.ts
  • meteor/server/publications/showStyleUI.ts
  • packages/blueprints-integration/src/api/showStyle.ts
  • packages/blueprints-integration/src/documents/piece.ts
  • packages/corelib/src/dataModel/ShowStyleBase.ts
  • packages/job-worker/src/blueprints/context/lib.ts
  • packages/meteor-lib/src/api/showStyles.ts
  • packages/webui/src/__mocks__/helpers/database.ts
  • packages/webui/src/client/ui/ClockView/DirectorScreen.tsx
  • packages/webui/src/client/ui/Settings/SettingsMenu.tsx
  • packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.scss
  • packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.tsx
  • packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx
  • packages/webui/src/client/ui/Settings/components/ColumnPackedGrid.scss
  • packages/webui/src/client/ui/Settings/components/ColumnPackedGrid.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
packages/blueprints-integration/src/api/showStyle.ts (1)
packages/blueprints-integration/src/content.ts (1)
  • SourceLayerType (212-212)
packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx (1)
packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.tsx (1)
  • AbChannelDisplaySettings (17-251)
meteor/server/publications/showStyleUI.ts (3)
packages/corelib/src/lib.ts (1)
  • literal (27-27)
packages/corelib/src/mongo.ts (1)
  • MongoFieldSpecifierOnesStrict (29-35)
packages/corelib/src/dataModel/ShowStyleBase.ts (1)
  • DBShowStyleBase (28-78)
packages/meteor-lib/src/api/showStyles.ts (1)
packages/corelib/src/dataModel/ShowStyleBase.ts (1)
  • DBShowStyleBase (28-78)
🪛 Biome (2.1.2)
packages/webui/src/client/ui/ClockView/DirectorScreen.tsx

[error] 446-446: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 475-475: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (19)
packages/blueprints-integration/src/documents/piece.ts (1)

48-53: LGTM!

The new displayAbChannel optional property is well-documented and appropriately typed as an optional boolean for overriding show style configuration at the piece level.

packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.scss (1)

1-21: LGTM!

Clean SCSS implementation following BEM conventions. The flexbox layout for the header and modifier pattern for section gaps are well-structured.

packages/webui/src/__mocks__/helpers/database.ts (2)

214-214: LGTM!

Correctly initializes the new abChannelDisplay field as undefined in the default mock, maintaining consistency with the DBShowStyleBase type.


558-558: LGTM!

Properly propagates abChannelDisplay from DBShowStyleBase to UIShowStyleBase in the conversion function, consistent with the mapping pattern used for other fields.

packages/blueprints-integration/src/api/showStyle.ts (2)

40-40: LGTM!

Import correctly added for SourceLayerType used in the new configuration structure.


326-338: LGTM!

Well-documented configuration structure for AB channel display. The design requiring all fields when the object is present ensures blueprints explicitly configure all filtering dimensions.

The forward-looking comment about future screen types (Line 337) is helpful for maintainability.

Empty array semantics are handled correctly: empty sourceLayerIds and outputLayerIds mean "no filtering" / "show all", as explicitly documented in the consumption logic at line 100 of DirectorScreen.tsx. The filtering is implemented as inclusive filters (OR logic for source layers when multiple filters exist), with clear comments explaining the behavior.

packages/job-worker/src/blueprints/context/lib.ts (2)

107-107: LGTM!

Correctly adds displayAbChannel to the allowlist of blueprint piece keys, following the established pattern for property registration.


256-256: LGTM!

Properly propagates displayAbChannel from the piece to the blueprint representation in convertPieceToBlueprints, consistent with the mapping pattern used for other piece properties.

packages/webui/src/client/ui/Settings/components/ColumnPackedGrid.tsx (1)

1-237: Well-structured reusable component with good TypeScript generics.

The component is well-designed with:

  • Clear interface definitions with JSDoc comments
  • Generic type support for flexibility
  • Proper separation into sub-components
  • Accessible checkbox patterns using <label> wrapping
packages/meteor-lib/src/api/showStyles.ts (1)

52-54: Good use of indexed access type to maintain type consistency.

Using DBShowStyleBase['abChannelDisplay'] ensures that UIShowStyleBase.abChannelDisplay stays in sync with the source of truth in the data model. This is a maintainable approach.

meteor/__mocks__/helpers/database.ts (1)

895-903: Mock correctly mirrors production mapping.

The addition of abChannelDisplay in the mock's convertToUIShowStyleBase function matches the production publication logic in meteor/server/publications/showStyleUI.ts. The use of Complete<UIShowStyleBase> ensures TypeScript will flag if any required fields are missing.

packages/webui/src/client/ui/Settings/SettingsMenu.tsx (1)

354-370: Navigation item correctly integrated.

The new "AB Channel Display" menu item follows the established pattern for show style settings navigation. The subPath matches the route definition in ShowStyleBaseSettings.tsx.

packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx (1)

13-13: Route correctly wired following established patterns.

The import and route for AbChannelDisplaySettings follow the same pattern as other settings panels (e.g., HotkeyLegendSettings). The route path /ab-channel-display matches the menu's subPath in SettingsMenu.tsx.

Also applies to: 135-137

packages/corelib/src/dataModel/ShowStyleBase.ts (1)

51-62: Good documentation and extensibility consideration.

The inline comment on line 61 noting future fields (showOnPresenterScreen, showOnCameraScreen) is helpful for maintainers understanding the planned evolution of this feature.

meteor/server/publications/showStyleUI.ts (1)

29-43: LGTM! Field additions are consistent with the data model.

The abChannelDisplay field is correctly added to ShowStyleBaseFields, the MongoDB projection (fieldSpecifier), and properly propagated to the published UIShowStyleBase object on line 88. This aligns with the DBShowStyleBase interface changes.

packages/webui/src/client/ui/Settings/ShowStyle/AbChannelDisplay.tsx (1)

186-250: Well-structured settings UI component.

The component cleanly separates concerns with memoized computations, proper callback handling, and a clear JSX structure. The reset-to-blueprint functionality and toggle controls are implemented appropriately.

meteor/server/migration/upgrades/showStyleBase.ts (1)

143-150: Good blueprint-vs-user override handling.

The logic correctly stores the blueprint default in blueprintAbChannelDisplay (enabling reset functionality) while only initializing abChannelDisplay if the user hasn't already customized it. This preserves user overrides across blueprint upgrades.

packages/webui/src/client/ui/ClockView/DirectorScreen.tsx (2)

507-552: Well-structured AB channel rendering logic.

The precomputation of currentPlayerEl and nextPlayerEl before the JSX return improves readability. The fallback from SVG icons to text display for non-alphanumeric player IDs is a sensible approach.


70-74: Remove unnecessary as any cast on line 71.

The displayAbChannel property is properly defined in IBlueprintPiece and correctly included in the Piece and PieceInstancePiece types. The as any cast is unnecessary and bypasses type checking. Since pieceInstance.piece is typed as PieceInstancePiece, the property should be directly accessible without casting:

const piece = pieceInstance.piece
if (piece.displayAbChannel !== undefined) {
	return piece.displayAbChannel
}

Comment on lines 446 to 505
const currentClipPlayer: string | undefined = useTracker(() => {
if (!currentPartInstance || !currentShowStyleBase || !playlist?.assignedAbSessions) return undefined
const config = currentShowStyleBase.abChannelDisplay
const instances = PieceInstances.find({
partInstanceId: currentPartInstance.instance._id,
reset: { $ne: true },
}).fetch()
for (const pi of instances) {
// Use configuration to determine if this piece should display AB channel
if (!shouldDisplayAbChannel(pi, currentShowStyleBase, config)) continue
const ab = pi.piece.abSessions
if (!ab || ab.length === 0) continue
for (const s of ab) {
const pool = playlist.assignedAbSessions?.[s.poolName]
if (!pool) continue
const matches: ABSessionAssignment[] = []
for (const key in pool) {
const a = pool[key]
if (a && a.sessionName === s.sessionName) matches.push(a)
}
const live = matches.find((m) => !m.lookahead)
const la = matches.find((m) => m.lookahead)
if (live) return String(live.playerId)
if (la) return String(la.playerId)
}
}
return undefined
}, [currentPartInstance?.instance._id, currentShowStyleBase?._id, playlist?.assignedAbSessions])

const nextClipPlayer: string | undefined = useTracker(() => {
if (!nextPartInstance || !nextShowStyleBaseId || !playlist?.assignedAbSessions) return undefined
// We need the ShowStyleBase to resolve sourceLayer types
const ssb = UIShowStyleBases.findOne(nextShowStyleBaseId)
if (!ssb) return undefined
const config = ssb.abChannelDisplay
const instances = PieceInstances.find({
partInstanceId: nextPartInstance.instance._id,
reset: { $ne: true },
}).fetch()
for (const pi of instances) {
// Use configuration to determine if this piece should display AB channel
if (!shouldDisplayAbChannel(pi, ssb, config)) continue
const ab = pi.piece.abSessions
if (!ab || ab.length === 0) continue
for (const s of ab) {
const pool = playlist.assignedAbSessions?.[s.poolName]
if (!pool) continue
const matches: ABSessionAssignment[] = []
for (const key in pool) {
const a = pool[key]
if (a && a.sessionName === s.sessionName) matches.push(a)
}
const live = matches.find((m) => !m.lookahead)
const la = matches.find((m) => m.lookahead)
if (live) return String(live.playerId)
if (la) return String(la.playerId)
}
}
return undefined
}, [nextPartInstance?.instance._id, nextShowStyleBaseId, playlist?.assignedAbSessions])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: useTracker hooks called conditionally violate React's Rules of Hooks.

The useTracker hooks for currentClipPlayer and nextClipPlayer are called inside the conditional block if (playlist && playlistId && segments). React requires hooks to be called unconditionally and in the same order on every render. This will cause state corruption, crashes, or undefined behavior.

Suggested fix: Move hooks outside the conditional

Move the useTracker calls before the conditional block and handle the undefined cases inside the hook callbacks:

 function DirectorScreenRender({
   playlist,
   segments,
   currentShowStyleBaseId,
   currentShowStyleBase,
   nextShowStyleBaseId,
   playlistId,
   currentPartInstance,
   currentSegment,
   nextPartInstance,
   nextSegment,
   rundownIds,
 }: Readonly<DirectorScreenProps & DirectorScreenTrackedProps>) {
   useSetDocumentClass('dark', 'xdark')
   const { t } = useTranslation()

   const timingDurations = useTiming()

+  // Compute current and next clip player ids (for pieces with AB sessions)
+  // Note: Hooks must be called unconditionally per React rules
+  const currentClipPlayer: string | undefined = useTracker(() => {
+    if (!currentPartInstance || !currentShowStyleBase || !playlist?.assignedAbSessions) return undefined
+    const config = currentShowStyleBase.abChannelDisplay
+    const instances = PieceInstances.find({
+      partInstanceId: currentPartInstance.instance._id,
+      reset: { $ne: true },
+    }).fetch()
+    for (const pi of instances) {
+      if (!shouldDisplayAbChannel(pi, currentShowStyleBase, config)) continue
+      const ab = pi.piece.abSessions
+      if (!ab || ab.length === 0) continue
+      for (const s of ab) {
+        const pool = playlist.assignedAbSessions?.[s.poolName]
+        if (!pool) continue
+        const matches: ABSessionAssignment[] = []
+        for (const key in pool) {
+          const a = pool[key]
+          if (a && a.sessionName === s.sessionName) matches.push(a)
+        }
+        const live = matches.find((m) => !m.lookahead)
+        const la = matches.find((m) => m.lookahead)
+        if (live) return String(live.playerId)
+        if (la) return String(la.playerId)
+      }
+    }
+    return undefined
+  }, [currentPartInstance?.instance._id, currentShowStyleBase?._id, playlist?.assignedAbSessions])
+
+  const nextClipPlayer: string | undefined = useTracker(() => {
+    if (!nextPartInstance || !nextShowStyleBaseId || !playlist?.assignedAbSessions) return undefined
+    const ssb = UIShowStyleBases.findOne(nextShowStyleBaseId)
+    if (!ssb) return undefined
+    const config = ssb.abChannelDisplay
+    const instances = PieceInstances.find({
+      partInstanceId: nextPartInstance.instance._id,
+      reset: { $ne: true },
+    }).fetch()
+    for (const pi of instances) {
+      if (!shouldDisplayAbChannel(pi, ssb, config)) continue
+      const ab = pi.piece.abSessions
+      if (!ab || ab.length === 0) continue
+      for (const s of ab) {
+        const pool = playlist.assignedAbSessions?.[s.poolName]
+        if (!pool) continue
+        const matches: ABSessionAssignment[] = []
+        for (const key in pool) {
+          const a = pool[key]
+          if (a && a.sessionName === s.sessionName) matches.push(a)
+        }
+        const live = matches.find((m) => !m.lookahead)
+        const la = matches.find((m) => m.lookahead)
+        if (live) return String(live.playerId)
+        if (la) return String(la.playerId)
+      }
+    }
+    return undefined
+  }, [nextPartInstance?.instance._id, nextShowStyleBaseId, playlist?.assignedAbSessions])
+
   if (playlist && playlistId && segments) {
     const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) || 0
     // ... rest of the conditional logic
-    // Move the useTracker calls from here to above the if statement

The hooks already handle the case when dependencies are undefined by returning undefined early, so moving them outside the conditional is safe.

🧰 Tools
🪛 Biome (2.1.2)

[error] 446-446: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 475-475: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In @packages/webui/src/client/ui/ClockView/DirectorScreen.tsx around lines 446 -
505, The two useTracker hooks (for currentClipPlayer and nextClipPlayer) are
being invoked conditionally inside the playlist/playlistId/segments guard which
violates React's Rules of Hooks; move the useTracker(...) calls out of that
conditional so they run unconditionally on every render (keep the existing
early-return checks inside each hook callback that return undefined when
currentPartInstance/nextPartInstance, showStyle data, or
playlist?.assignedAbSessions are missing), ensuring the hooks remain in the same
order and then use the resulting currentClipPlayer and nextClipPlayer values
within the original conditional block.

Comment on lines 32 to 44
[SourceLayerType.UNKNOWN]: 'Unknown',
[SourceLayerType.CAMERA]: 'Camera',
[SourceLayerType.VT]: 'VT',
[SourceLayerType.REMOTE]: 'Remote',
[SourceLayerType.SCRIPT]: 'Script',
[SourceLayerType.GRAPHICS]: 'Graphics',
[SourceLayerType.SPLITS]: 'Splits',
[SourceLayerType.AUDIO]: 'Audio',
[SourceLayerType.LOWER_THIRD]: 'Lower Third',
[SourceLayerType.LIVE_SPEAK]: 'Live Speak',
[SourceLayerType.TRANSITION]: 'Transition',
[SourceLayerType.LIGHTS]: 'Lights',
[SourceLayerType.LOCAL]: 'Local',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These strings aren't exposed to i18n and there's possibility of duplication/misalignment if these are changed/new SourceLayerTypes are added.

It seems that there already exists a sourceLayerString function inside of client/ui/Settings/ShowStyle/SourceLayer.tsx that does the same thing, but ensures that the strings are translatable. It's not exported, but could be moved to a library function and reused here.

Comment on lines 71 to 73
const GROUP_HEADER_HEIGHT = 40 // Height for group checkbox + label
const ITEM_HEIGHT_BASE = 30 // Base height for checkbox + label
const GROUP_SPACING = 10 // Extra spacing after group
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that these are component-global assumptions I would put these at the top of the file, explaining what they are.


const timingDurations = useTiming()

if (playlist && playlistId && segments) {
Copy link
Contributor

@jstarpl jstarpl Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't noticed this before, but this breaks React's rule of hooks for later uses of useTracker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines 468 to 469
if (live) return String(live.playerId)
if (la) return String(la.playerId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should be unprotectString not a String cast.

@rjmunro rjmunro force-pushed the rjmunro/directors-view-show-ab branch from 4a00709 to e07e230 Compare January 13, 2026 17:13
@rjmunro rjmunro force-pushed the rjmunro/directors-view-show-ab branch from e07e230 to 16ca0ca Compare January 13, 2026 17:23
Copy link
Contributor Author

@rjmunro rjmunro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All fixed.


const timingDurations = useTiming()

if (playlist && playlistId && segments) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@rjmunro rjmunro requested a review from jstarpl January 14, 2026 11:13
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@rjmunro rjmunro merged commit d833f9d into release53 Jan 16, 2026
69 of 71 checks passed
@rjmunro rjmunro deleted the rjmunro/directors-view-show-ab branch January 16, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Contribution from BBC Contributions sponsored by BBC (bbc.co.uk)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants