Skip to content

Conversation

@rjmunro
Copy link
Contributor

@rjmunro rjmunro commented Oct 21, 2025

About the Contributor

This pull request is posted on behalf of the BBC.

Type of Contribution

This is a Feature.

Current Behavior

Items in the timeline are fixed.

New Behavior

If a piece has a userEditOperation of type UserEditingType.SOFIE and id DefaultUserOperationsTypes.RETIME_PIECE and editing is enabled, you can drag it around the timeline.

Dragging.demo.mov

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 the timeline view
  • This PR affects what blueprints can do

Time Frame

Other Information

This is a rebase of bbc#35 to the upstream release 53.

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.

UI Piece Retiming Feature

Adds support for dragging and retiming timeline pieces in the Rundown view UI when the feature is enabled through user edit operations.

Changes Overview

Blueprint Integration & Type Definitions

  • Introduces RETIME_PIECE operation type in DefaultUserOperationsTypes
  • Adds DefaultUserOperationRetimePiece interface with payload containing segmentExternalId, partExternalId, and inPoint
  • Updates DefaultUserOperations union to include the new operation type
  • Adds optional limitToCurrentPart field to UserEditingDefinitionSofieDefault and CoreUserEditingDefinitionSofie

Edit Mode Action Support

  • Adds editMode member to ClientActions enum
  • Introduces IEditModeAction interface with state (true/false/'toggle') and filter chain support
  • Creates EDIT_MODE event type in RundownViewEvents with EditModeEvent payload
  • Implements createEditModeAction for emitting edit mode events

Drag & Drop Implementation

  • Introduces drag context API (IDragContext, dragContext) to manage drag lifecycle
  • Implements DragContextProvider component that:
    • Tracks dragging state (piece ID, piece data, enabled status)
    • Handles mouse movement and drag end events
    • Performs piece retiming via Meteor call when drag completes
    • Integrates with RundownViewEventBus for edit mode toggling
    • Provides startDrag and setHoveredPart methods for drag management

UI Components

  • Updates RundownView to wrap content with DragContextProvider
  • Modifies SourceLayer to integrate with drag context, reordering pieces during drag operations and updating hover state
  • Updates SourceLayerItem to:
    • Detect draggable pieces via userEditOperations (retime piece operations)
    • Initiate drags with piece, timeScale, position, and part/segment constraints
    • Apply draggable-element className to draggable pieces
  • Updates pieceUiClassNames function to accept optional draggable parameter and apply styling

Styling & UI Editor

  • Adds .draggable-element SCSS class with dotted white border styling
  • Updates action editor (ActionSelector) to support editMode action configuration with state dropdown (Enable/Disable/Toggle)

Internal Implementation

  • Updates user edit translation functions in job-worker context library to handle limitToCurrentPart field and use explicit literal type wrappers

Current Status

  • Functionality tested by author
  • Test coverage at 0% (14 lines missing coverage in packages/job-worker/src/blueprints/context/lib.ts)
  • Unit tests and documentation not yet completed
  • PR marked as Draft

@codecov-commenter
Copy link

codecov-commenter commented Oct 21, 2025

Codecov Report

❌ Patch coverage is 50.00000% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/job-worker/src/blueprints/context/lib.ts 50.00% 6 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@rjmunro rjmunro force-pushed the feat/ui-piece-retiming branch from 320f3d4 to 7e84b20 Compare October 22, 2025 12:11
@rjmunro rjmunro changed the title Feat/UI piece retiming feat: UI piece retiming Oct 23, 2025
@Saftret Saftret added the Contribution from BBC Contributions sponsored by BBC (bbc.co.uk) label Dec 10, 2025
@rjmunro rjmunro force-pushed the feat/ui-piece-retiming branch from 7e84b20 to eadaa4c Compare January 7, 2026 19:05
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 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

Adds a RETIME_PIECE user operation and edit-mode action; extends core and blueprint typings; introduces a drag-and-drop context and provider in the Rundown UI to start retime interactions, emits edit-mode events, and calls Meteor to persist piece retimes.

Changes

Cohort / File(s) Summary
Blueprints & Core types
packages/blueprints-integration/src/ingest.ts, packages/corelib/src/dataModel/UserEditingDefinitions.ts
Added RETIME_PIECE to DefaultUserOperationsTypes; new DefaultUserOperationRetimePiece type and union membership; added optional limitToCurrentPart?: boolean to core Sofie user-editing definition.
User editing config (blueprints)
packages/blueprints-integration/src/userEditing.ts
Added optional limitToCurrentPart?: boolean to UserEditingDefinitionSofieDefault.
Triggered actions & events
packages/blueprints-integration/src/triggers.ts, packages/meteor-lib/src/triggers/RundownViewEventBus.ts, packages/meteor-lib/src/triggers/actionFactory.ts
Added IEditModeAction (state: true
Shared action enum
packages/shared-lib/src/core/model/ShowStyle.ts
Added editMode member to ClientActions enum.
Job-worker type construction
packages/job-worker/src/blueprints/context/lib.ts
Switched several object literal returns to literal<...>(...) wrappers and removed satisfies Complete<...> assertions; propagate limitToCurrentPart in SOFIE branches.
Drag context API & provider
packages/webui/src/client/ui/RundownView/DragContext.ts, packages/webui/src/client/ui/RundownView/DragContextProvider.tsx
Added IDragContext and exported dragContext; new DragContextProvider component managing drag lifecycle, hover tracking, mouse listeners, edit-mode emission, and Meteor call to persist RETIME_PIECE.
Rundown view integration
packages/webui/src/client/ui/RundownView.tsx, packages/webui/src/client/ui/RundownView/DragContextProvider.tsx
Wrapped Rundown view content with DragContextProvider; introduced DragContextProvider export and adjusted component composition and error boundaries; updated Shelf invocation props.
Segment/Source layer interaction
packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx, packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx
Integrated dragContext consumption: hover updates, piece reordering during drag, guarded drag start on mousedown, double-click prevention changes, and adjusted renderer keys/props to use computed elKey.
Piece UI classes & styles
packages/webui/src/client/lib/ui/pieceUiClassNames.ts, packages/webui/src/client/styles/elementSelected.scss
pieceUiClassNames gains optional draggable?: boolean and adds 'draggable-element' class when true; added .draggable-element { border: dotted white 1px; }.
Action editor UI
packages/webui/src/client/ui/Settings/.../ActionSelector.tsx
Added editor support for ClientActions.editMode with State dropdown (Enable, Disable, Toggle) and related labeling/argument handling.

Sequence Diagram

sequenceDiagram
    participant User
    participant SourceLayerItem
    participant DragContextProvider
    participant RundownViewEventBus
    participant MeteorBackend
    participant UI

    User->>SourceLayerItem: mousedown on draggable piece
    SourceLayerItem->>DragContextProvider: startDrag(piece, timeScale, position, elementOffset?, limitToPart?, limitToSegment?)
    DragContextProvider->>UI: enable drag visuals, set pieceId/piece
    User->>UI: move mouse over parts
    UI->>DragContextProvider: pointer move events (internal)
    SourceLayerItem->>DragContextProvider: setHoveredPart(partId, segmentId, position)
    DragContextProvider->>DragContextProvider: update hover state
    User->>DragContextProvider: mouseup
    DragContextProvider->>RundownViewEventBus: emit EDIT_MODE (state)
    DragContextProvider->>MeteorBackend: call retimeUserOperation(RETIME_PIECE, payload)
    MeteorBackend->>MeteorBackend: persist retime (update inPoint)
    DragContextProvider->>UI: clear drag visuals, reset state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I tug a piece across the line,

whiskers twitch, a tiny sign,
edit-mode hums, the timeline sighs,
retime found where pixels rise,
hop — the beat is all aligned.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% 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: UI piece retiming' accurately and concisely summarizes the main feature being added—the ability to retime pieces in the UI timeline, which is the core objective of this changeset.

✏️ 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 force-pushed the feat/ui-piece-retiming branch 3 times, most recently from 282eb15 to 8075b5b Compare January 8, 2026 11:52
@rjmunro rjmunro force-pushed the feat/ui-piece-retiming branch from 8075b5b to 97556fc Compare January 8, 2026 13:17
@rjmunro
Copy link
Contributor Author

rjmunro commented Jan 9, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 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.

@rjmunro rjmunro marked this pull request as ready for review January 9, 2026 15:17
@rjmunro rjmunro requested a review from a team as a code owner January 9, 2026 15:17
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: 4

🤖 Fix all issues with AI agents
In @packages/blueprints-integration/src/userEditing.ts:
- Around line 60-64: The field limitToCurrentPart is wrongly declared on
UserEditingDefinitionSofieDefault allowing it for all
DefaultUserOperationsTypes; change the types so limitToCurrentPart exists only
for RETIME_PIECE operations — either create a specific interface (e.g.,
RetimePieceUserEditingDefinition) that extends the base and adds
limitToCurrentPart, or convert UserEditingDefinitionSofieDefault into a
discriminated union keyed by the operation type (with a variant for RETIME_PIECE
that includes limitToCurrentPart), update any uses expecting
UserEditingDefinitionSofieDefault accordingly and keep runtime guards like in
SourceLayerItem.tsx in sync with the new discriminant.

In @packages/corelib/src/dataModel/UserEditingDefinitions.ts:
- Around line 50-55: There are two exported declarations of
CoreUserEditingDefinitionSofie which risks accidental interface-merging; remove
the duplicate declaration so only one exported interface
CoreUserEditingDefinitionSofie remains, keeping the correct version (the one
with type: UserEditingType.SOFIE and id: DefaultUserOperationsTypes) and delete
the redundant copy found later in the file.

In @packages/webui/src/client/ui/RundownView/DragContextProvider.tsx:
- Around line 92-97: The early return when Segments.findOne(part?.segmentId)
yields undefined leaves pieceId set and prevents future drags; update the block
that computes segment and oldSegment so that if segment is falsy you call
cleanup() before returning (i.e., call cleanup() when
Segments.findOne(part?.segmentId) returns undefined), ensuring pieceId is
cleared and the drag-start guard (the pieceId check) is reset; use the existing
cleanup() helper rather than duplicating state logic and keep oldSegment
resolution unchanged.

In @packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx:
- Around line 95-107: The hover handler uses e.target which can be a child
element and yields wrong coords; change the bounding rect call in onMouseEnter
to use e.currentTarget (e.g., (e.currentTarget as
HTMLDivElement).getBoundingClientRect()) so the rect is for the layer container,
then pass those coords to dragCtx.setHoveredPart as before; apply the same
change to the other mouse event handler at the similar location that currently
uses e.target.
🧹 Nitpick comments (4)
packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx (1)

394-427: Inconsistent styling with similar components.

The edit mode parameter editor deviates from the pattern established by the shelf action:

  1. Line 396: Uses className="mts" instead of className="mt-2" (compare to shelf at line 362)
  2. Line 399: Uses classNames="input text-input input-m" instead of classNames="mb-2" (compare to shelf at line 365)

Consider aligning the styling with the shelf implementation for consistency unless there's a specific reason for the divergence.

♻️ Align styling with shelf pattern
 		case ClientActions.editMode:
 			return (
-				<div className="mts">
+				<div className="mt-2">
 					<label className="block">{t('State')}</label>
 					<DropdownInputControl<typeof action.state>
-						classNames="input text-input input-m"
+						classNames="mb-2"
 						value={action.state}
 						// placholder={t('State')}
 						options={[
packages/webui/src/client/lib/ui/pieceUiClassNames.ts (1)

10-24: Backwards-compatible draggable styling hook looks good

Optional param won’t break existing call sites, and the class toggle is idiomatic for classnames.

Optional: consider an options object to stop the param list growing

The function is already at many positional params; a { uiState, draggable, ... } options object would be easier to extend without forcing undefined placeholders.

Also applies to: 60-63

packages/webui/src/client/ui/RundownView/DragContextProvider.tsx (2)

35-139: Consider memoizing startDrag with useCallback.

The startDrag function is recreated on every render because it's not wrapped in useCallback. This causes the context value (ctx) to change on every render, which may trigger unnecessary re-renders in consuming components. While not critical, this could impact performance with many consumers.

♻️ Suggested refactor
-	const startDrag = (
+	const startDrag = useCallback((
 		ogPiece: PieceUi,
 		timeScale: number,
 		pos: { x: number; y: number },
 		elementOffset?: number,
 		limitToPart?: PartInstanceId,
 		limitToSegment?: SegmentId
 	) => {
 		// ... function body unchanged
-	}
+	}, [pieceId, t])

Note: You'll also need to memoize setHoveredPart and use useMemo for ctx to get the full benefit.


132-138: Timeout cleanup doesn't clear the timeout handle on successful completion.

The 10-second timeout will still fire and call cleanup() again even if the drag completed successfully via onMouseUp. While cleanup() is idempotent, storing and clearing the timeout handle would be cleaner.

♻️ Suggested improvement
+		let timeoutHandle: ReturnType<typeof setTimeout> | undefined
+
 		const onMouseUp = (e: MouseEvent) => {
 			// detach from the mouse
 			document.removeEventListener('mousemove', onMove)
 			document.removeEventListener('mouseup', onMouseUp)
+			if (timeoutHandle) clearTimeout(timeoutHandle)

 			// ... rest of function
 		}

 		document.addEventListener('mousemove', onMove)
 		document.addEventListener('mouseup', onMouseUp)

-		setTimeout(() => {
+		timeoutHandle = setTimeout(() => {
 			// after the timeout we want to bail out in case something went wrong
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e012da and 97556fc.

📒 Files selected for processing (16)
  • packages/blueprints-integration/src/ingest.ts
  • packages/blueprints-integration/src/triggers.ts
  • packages/blueprints-integration/src/userEditing.ts
  • packages/corelib/src/dataModel/UserEditingDefinitions.ts
  • packages/job-worker/src/blueprints/context/lib.ts
  • packages/meteor-lib/src/triggers/RundownViewEventBus.ts
  • packages/meteor-lib/src/triggers/actionFactory.ts
  • packages/shared-lib/src/core/model/ShowStyle.ts
  • packages/webui/src/client/lib/ui/pieceUiClassNames.ts
  • packages/webui/src/client/styles/elementSelected.scss
  • packages/webui/src/client/ui/RundownView.tsx
  • packages/webui/src/client/ui/RundownView/DragContext.ts
  • packages/webui/src/client/ui/RundownView/DragContextProvider.tsx
  • packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx
  • packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx
  • packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
packages/webui/src/client/ui/RundownView/DragContextProvider.tsx (9)
packages/shared-lib/src/core/model/Ids.ts (2)
  • PieceInstanceId (15-15)
  • PartInstanceId (18-18)
packages/corelib/src/dataModel/Ids.ts (1)
  • SegmentId (67-67)
packages/webui/src/client/ui/Collections.tsx (1)
  • UIParts (48-48)
packages/webui/src/client/lib/clientUserAction.ts (2)
  • doUserAction (154-260)
  • UserAction (13-13)
packages/corelib/src/lib.ts (1)
  • literal (27-27)
packages/blueprints-integration/src/ingest.ts (1)
  • DefaultUserOperationRetimePiece (165-174)
packages/meteor-lib/src/triggers/RundownViewEventBus.ts (1)
  • EditModeEvent (78-80)
packages/webui/src/meteor/minimongo/common.js (1)
  • s (1439-1439)
packages/webui/src/client/ui/RundownView/DragContext.ts (2)
  • IDragContext (5-43)
  • dragContext (45-45)
packages/webui/src/client/ui/RundownView/DragContext.ts (2)
packages/shared-lib/src/core/model/Ids.ts (2)
  • PartInstanceId (18-18)
  • PieceInstanceId (15-15)
packages/corelib/src/dataModel/Ids.ts (1)
  • SegmentId (67-67)
packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx (3)
packages/webui/src/client/ui/RundownView/DragContext.ts (1)
  • dragContext (45-45)
packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts (1)
  • pieces (74-76)
packages/webui/src/client/lib/lib.tsx (1)
  • contextMenuHoldToDisplayTime (27-29)
packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx (3)
packages/webui/src/client/ui/RundownView/DragContext.ts (1)
  • dragContext (45-45)
packages/shared-lib/src/lib/protectedString.ts (1)
  • unprotectString (35-37)
packages/webui/src/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx (1)
  • MicSourceRenderer (15-227)
packages/webui/src/client/ui/RundownView.tsx (2)
packages/webui/src/client/ui/RundownView/DragContextProvider.tsx (1)
  • DragContextProvider (24-176)
packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx (1)
  • SelectedElementsContext (137-137)
🔇 Additional comments (20)
packages/meteor-lib/src/triggers/RundownViewEventBus.ts (1)

32-32: LGTM! Consistent event infrastructure.

The new EDIT_MODE event follows the established pattern for similar events (e.g., SHELF_STATE). The interface and event bus mapping are properly integrated.

Also applies to: 78-80, 147-147

packages/webui/src/client/styles/elementSelected.scss (1)

22-24: LGTM! Clean CSS addition.

The .draggable-element class provides clear visual indication for draggable items.

packages/shared-lib/src/core/model/ShowStyle.ts (1)

110-110: LGTM! Enum value added correctly.

The new editMode action is properly added to the ClientActions enum.

packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx (1)

96-106: LGTM! Edit mode handling follows established patterns.

The implementation for getArguments, hasArguments, and actionToLabel correctly mirrors the pattern used for the shelf action with proper state handling (Enable/Disable/Toggle).

Also applies to: 161-162, 209-210

packages/blueprints-integration/src/triggers.ts (2)

275-279: IEditModeAction addition looks consistent with other GUI-only actions

Matches the existing IShelfAction pattern (GUI-only filterChain, state supports toggle), so this should integrate cleanly.


320-341: SomeAction union extension is correct

Including IEditModeAction in SomeAction is the right wiring to make the action usable in triggered actions.

Also applies to: 334-335

packages/meteor-lib/src/triggers/actionFactory.ts (2)

453-458: Switch wiring for ClientActions.editMode looks correct

Routes to the new action creator the same way as ClientActions.shelf.


289-298: createEditModeAction implementation is sound—event is properly consumed

The EDIT_MODE event is defined in RundownViewEventBus, emitted in this action, and has an active listener in DragContextProvider.tsx (onSetEditMode handler) that applies the state. No further action needed.

packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx (1)

97-101: Drag-aware pieces list composition is intentional and correctly implemented

The conditional injection of the dragged piece only in its source layer (when dragCtx.piece.sourceLayer?._id === props.layer._id) is the intended design. The setHoveredPart() call only updates internal tracking data for hover position and part, not visual rendering. No ghost or preview rendering is implemented elsewhere in the codebase—the dragged piece stays visually in its source layer during hover, which is the expected behavior.

packages/webui/src/client/ui/RundownView/DragContext.ts (1)

1-45: LGTM! Clean interface design for drag context.

The interface is well-documented with clear JSDoc comments. The context typing as IDragContext | undefined correctly allows for cases where the context is not provided.

Minor notes:

  • Line 12: The comment "consider omitting this is a todo" reads awkwardly. Consider rewording to "consider omitting this. This is a TODO" or similar.
  • Line 45: The informal comment // slay. could be removed for consistency.
packages/blueprints-integration/src/ingest.ts (2)

127-134: LGTM! Consistent enum extension.

The new RETIME_PIECE enum value follows the established naming convention with the __sofie- prefix.


165-182: LGTM! Well-structured operation type.

The DefaultUserOperationRetimePiece type follows the existing pattern for other operations. The comment about future duration support is helpful for maintainability.

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

527-555: LGTM! Consistent use of literal<>() pattern.

The refactor from satisfies Complete<...> to literal<...>() aligns with the existing patterns in this file (e.g., line 152 literal<Complete<IBlueprintPieceInstance['infinite']>>). This maintains type safety while improving consistency.


586-618: LGTM! Symmetric refactor in translateUserEditsFromBlueprint.

The changes mirror those in translateUserEditsToBlueprint, maintaining consistency across both translation directions.

packages/webui/src/client/ui/RundownView.tsx (2)

1371-1580: LGTM! Proper integration of DragContextProvider.

The DragContextProvider is correctly placed to wrap the SelectedElementsContext.Consumer, ensuring drag context is available to all child components. The ErrorBoundary wrapping around various UI sections provides good error isolation.


1408-1422: All Shelf component props are correctly typed.

Verification confirms that onChangeBottomMargin, rundownLayout, and studio are all properly declared in the IShelfProps interface with appropriate types:

  • onChangeBottomMargin?: (newBottomMargin: string) => void
  • rundownLayout?: RundownLayoutShelfBase
  • studio: UIStudio

No issues found.

packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx (4)

124-127: LGTM! Clear logic for determining draggable state.

The check correctly identifies pieces with the RETIME_PIECE operation, ensuring only appropriately configured pieces can be dragged.


568-593: LGTM! Clean key extraction for renderers.

Extracting elKey separately from the spread props is the correct approach since React keys cannot be spread. The comment on line 568-569 provides good documentation.


203-206: The as any cast could be replaced with a type guard, but limitToCurrentPart is already defined in the type.

The property limitToCurrentPart?: boolean is already present in the UserEditingDefinitionSofieDefault interface (line 64 of packages/blueprints-integration/src/userEditing.ts). The as any cast exists because the find() method returns a union type (UserEditingDefinition) that TypeScript cannot automatically narrow based on the condition alone.

A better approach would be to use a type guard instead of the as any cast, but this is a minor code quality improvement rather than a missing type definition issue.

Likely an incorrect or invalid review comment.


614-618: The undefined argument on line 616 is intentional and correctly corresponds to the optional uiState parameter in the function signature. The inline comment // this.state already documents its purpose, indicating this parameter previously held component state but is now intentionally omitted—likely due to refactoring away from class component state. No change needed.

Likely an incorrect or invalid review comment.

Copy link
Member

@nytamin nytamin left a comment

Choose a reason for hiding this comment

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

Looks good! I left a few minor comments.

Comment on lines 30 to 33
/**
* Whether dragging is enabled
*/
enabled: boolean
Copy link
Member

Choose a reason for hiding this comment

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

Minor note: The meaning "enabled" could be misunderstood at a glance (does it mean that an element can be dragged, or is currently being dragged? Perhaps clarify the description?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines 76 to 77
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onMouseUp)
Copy link
Member

Choose a reason for hiding this comment

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

DRY: Should the removal of the events be moved to the cleanup() function (alternatively another cleanupEvents() function? If yes, also make sure that the cleanup is run upon every return from this function, and in the setTimeout further down.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

const segmentIdRef = useRef<undefined | SegmentId>(undefined)
const limitToPartRef = useRef<undefined | PartInstanceId>(undefined)

const startDrag = (
Copy link
Member

Choose a reason for hiding this comment

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

Minor note: I don't know if this is true for ContextProviders or not, but should the methods in this component be wrapped in a React.useCallback for performance reasons?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@rjmunro rjmunro force-pushed the feat/ui-piece-retiming branch from 97556fc to 99b01d6 Compare January 12, 2026 18:16
@rjmunro
Copy link
Contributor Author

rjmunro 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: 2

🤖 Fix all issues with AI agents
In @packages/webui/src/client/ui/RundownView/DragContextProvider.tsx:
- Around line 132-135: The untracked setTimeout in DragContextProvider that
calls cleanup() after DRAG_TIMEOUT can clear state for subsequent drags; store
the timeout ID when you call setTimeout (e.g., local variable or ref like
dragTimeoutId) and clear it with clearTimeout(...) whenever a new drag starts or
when cleanup() runs (also set the ref to null after clearing) so the timeout for
an earlier drag cannot fire during a later drag; update the code paths around
setTimeout, cleanup(), and the drag-start logic to set/clear this tracked
timeout.

In @packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx:
- Around line 195-224: The useCallback for itemMouseDown captures
part.instance.segmentId but part is missing from the dependency array; update
the useCallback dependency array for itemMouseDown to include part (alongside
piece, timeScale, dragCtx and any other used external symbols like
hasDraggableElement) so the closure always sees the current
part.instance.segmentId when calling dragCtx.startDrag.
🧹 Nitpick comments (4)
packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx (1)

394-427: Consider aligning CSS classes with the shelf action editor for consistency.

The editMode editor uses className="mts" and classNames="input text-input input-m" while the adjacent shelf editor (lines 361-393) uses className="mt-2" and classNames="mb-2". This may result in inconsistent spacing and styling between similar action editors.

Suggested fix for consistency
 		case ClientActions.editMode:
 			return (
-				<div className="mts">
+				<div className="mt-2">
 					<label className="block">{t('State')}</label>
 					<DropdownInputControl<typeof action.state>
-						classNames="input text-input input-m"
+						classNames="mb-2"
 						value={action.state}
packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx (1)

97-100: Consider potential performance concern with piece reordering on every render.

The pieces computation runs on every render. When a drag is active (dragCtx?.piece), it filters and concatenates arrays. For layers with many pieces, consider memoizing this computation.

Optional memoization
+import React, { MouseEventHandler, useCallback, useContext, useMemo, useState } from 'react'
...
-	const pieces =
-		dragCtx?.piece && dragCtx.piece.sourceLayer?._id === props.layer._id
-			? (props.layer.pieces ?? []).filter((p) => p.instance._id !== dragCtx.piece?.instance._id).concat(dragCtx.piece)
-			: props.layer.pieces
+	const pieces = useMemo(() => {
+		if (dragCtx?.piece && dragCtx.piece.sourceLayer?._id === props.layer._id) {
+			return (props.layer.pieces ?? [])
+				.filter((p) => p.instance._id !== dragCtx.piece?.instance._id)
+				.concat(dragCtx.piece)
+		}
+		return props.layer.pieces
+	}, [dragCtx?.piece, props.layer._id, props.layer.pieces])
packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx (2)

124-127: Consider extracting the retime operation once to avoid duplicate lookups.

The same find operation to locate the retime user operation is performed here and again in itemMouseDown (lines 204-206). Consider storing the found operation to avoid the duplicate search.

♻️ Suggested refactor
 	const dragCtx = useContext(dragContext)
-	const hasDraggableElement = !!piece.instance.piece.userEditOperations?.find(
+	const retimeOperation = piece.instance.piece.userEditOperations?.find(
 		(op) => op.type === UserEditingType.SOFIE && op.id === DefaultUserOperationsTypes.RETIME_PIECE
 	)
+	const hasDraggableElement = !!retimeOperation

Then in itemMouseDown, use retimeOperation directly instead of searching again.


204-208: Consider using proper typing instead of as any.

The as any type assertion on line 206 bypasses TypeScript's type checking. If limitToCurrentPart is a known property on the RETIME_PIECE operation type, consider using a more specific type assertion or type guard.

♻️ Example approach
-			const retimeOp = piece.instance.piece.userEditOperations?.find(
-				(op) => op.type === UserEditingType.SOFIE && op.id === DefaultUserOperationsTypes.RETIME_PIECE
-			) as any
+			const retimeOp = piece.instance.piece.userEditOperations?.find(
+				(op): op is UserEditingDefinitionSofie & { limitToCurrentPart?: boolean } =>
+					op.type === UserEditingType.SOFIE && op.id === DefaultUserOperationsTypes.RETIME_PIECE
+			)

Alternatively, define a proper type for the RETIME_PIECE operation that includes limitToCurrentPart.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97556fc and 99b01d6.

📒 Files selected for processing (16)
  • packages/blueprints-integration/src/ingest.ts
  • packages/blueprints-integration/src/triggers.ts
  • packages/blueprints-integration/src/userEditing.ts
  • packages/corelib/src/dataModel/UserEditingDefinitions.ts
  • packages/job-worker/src/blueprints/context/lib.ts
  • packages/meteor-lib/src/triggers/RundownViewEventBus.ts
  • packages/meteor-lib/src/triggers/actionFactory.ts
  • packages/shared-lib/src/core/model/ShowStyle.ts
  • packages/webui/src/client/lib/ui/pieceUiClassNames.ts
  • packages/webui/src/client/styles/elementSelected.scss
  • packages/webui/src/client/ui/RundownView.tsx
  • packages/webui/src/client/ui/RundownView/DragContext.ts
  • packages/webui/src/client/ui/RundownView/DragContextProvider.tsx
  • packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx
  • packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx
  • packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/shared-lib/src/core/model/ShowStyle.ts
  • packages/meteor-lib/src/triggers/RundownViewEventBus.ts
  • packages/corelib/src/dataModel/UserEditingDefinitions.ts
  • packages/webui/src/client/ui/RundownView/DragContext.ts
  • packages/blueprints-integration/src/triggers.ts
  • packages/webui/src/client/lib/ui/pieceUiClassNames.ts
🧰 Additional context used
🧬 Code graph analysis (4)
packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx (3)
packages/webui/src/client/ui/RundownView/DragContext.ts (1)
  • dragContext (46-46)
packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts (1)
  • pieces (74-76)
packages/webui/src/client/lib/lib.tsx (1)
  • contextMenuHoldToDisplayTime (27-29)
packages/webui/src/client/ui/RundownView.tsx (2)
packages/webui/src/client/ui/RundownView/DragContextProvider.tsx (1)
  • DragContextProvider (24-183)
packages/webui/src/client/ui/RundownView/SelectedElementsContext.tsx (1)
  • SelectedElementsContext (137-137)
packages/job-worker/src/blueprints/context/lib.ts (5)
packages/corelib/src/lib.ts (2)
  • literal (27-27)
  • omit (36-38)
packages/blueprints-integration/src/userEditing.ts (3)
  • UserEditingDefinitionAction (18-36)
  • UserEditingDefinitionForm (41-51)
  • UserEditingDefinitionSofieDefault (56-65)
packages/corelib/src/dataModel/UserEditingDefinitions.ts (3)
  • CoreUserEditingDefinitionAction (15-31)
  • CoreUserEditingDefinitionForm (36-48)
  • CoreUserEditingDefinitionSofie (95-104)
packages/corelib/src/TranslatableMessage.ts (1)
  • wrapTranslatableMessageFromBlueprints (102-110)
packages/shared-lib/src/lib/protectedString.ts (1)
  • unprotectStringArray (38-40)
packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx (2)
packages/webui/src/client/ui/RundownView/DragContext.ts (1)
  • dragContext (46-46)
packages/shared-lib/src/lib/protectedString.ts (1)
  • unprotectString (35-37)
🔇 Additional comments (17)
packages/meteor-lib/src/triggers/actionFactory.ts (2)

289-298: Implementation follows established pattern correctly.

The createEditModeAction function mirrors the structure of createShelfAction and other similar action creators in this file. The implementation is consistent with the codebase conventions.

Note: The AI summary claims there are "two identical function declarations" of createEditModeAction, but only one definition is present in the code. This appears to be an AI summary inconsistency.


456-457: LGTM!

The switch case correctly delegates to createEditModeAction following the same pattern as the adjacent ClientActions.shelf case. The ClientActions.editMode enum is properly defined in shared-lib, the createEditModeAction function has the correct signature, and the action type interface includes both required state and filterChain properties.

packages/webui/src/client/styles/elementSelected.scss (1)

22-24: LGTM!

Simple CSS addition for draggable element visual feedback. The dotted border provides clear drag affordance.

packages/blueprints-integration/src/userEditing.ts (1)

60-64: LGTM!

The optional limitToCurrentPart property is well-documented and appropriately scoped to RETIME_PIECE operations.

packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx (1)

96-106: LGTM!

The editMode case correctly mirrors the existing shelf action pattern with appropriate Enable/Disable/Toggle labels and exhaustive state handling via assertNever.

packages/blueprints-integration/src/ingest.ts (1)

165-174: LGTM!

The DefaultUserOperationRetimePiece type is well-structured and follows the established pattern for default user operations. The comment noting future duration support is helpful for maintainability.

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

544-549: LGTM!

The limitToCurrentPart property is correctly mapped in both translateUserEditsToBlueprint and translateUserEditsFromBlueprint functions, ensuring proper bidirectional conversion between core and blueprint types.

packages/webui/src/client/ui/SegmentTimeline/Parts/SourceLayer.tsx (1)

102-107: LGTM!

The onMouseEnter handler correctly updates the drag context with the hovered part information, enabling cross-part dragging. The early return when dragCtx is undefined is appropriate defensive coding.

packages/webui/src/client/ui/RundownView/DragContextProvider.tsx (4)

140-151: LGTM!

The setHoveredPart callback has appropriate guards: checking for active drag (pieceId), avoiding redundant updates (same part), and respecting segment/part constraints via refs.


161-166: LGTM!

Proper cleanup pattern for the event bus subscription using the useEffect return function.


67-116: This review comment is based on a misunderstanding of doUserAction's behavior.

The doUserAction function already calls the callback in all cases: when there's an error response, when the operation succeeds, and when an exception is caught in the promise .catch() block. The callback parameter (err: any, res?: Result) => void | boolean is invoked with an error argument in both the error response path (line 195: callback(res)) and the exception path (line 241: callback(err)).

Therefore, cleanup() will be called regardless of success or failure—no additional error handling is needed.

Likely an incorrect or invalid review comment.


23-23: The cross-rundown drag constraint already exists in the codebase. Dragging is limited to segments via segmentIdRef in setHoveredPart (line 144), which effectively prevents cross-rundown dragging since each segment belongs to exactly one rundown. The comment at line 23 is outdated and does not reflect the current implementation.

Likely an incorrect or invalid review comment.

packages/webui/src/client/ui/RundownView.tsx (2)

1371-1380: LGTM: DragContextProvider integration looks correct.

The DragContextProvider is properly positioned in the component hierarchy, wrapping the content that needs drag context access. The t translation function is correctly passed from the translated component.


1386-1422: LGTM: Error boundary wrappers and component structure.

The ErrorBoundary components provide appropriate error isolation for each major UI section, preventing failures in one component from crashing the entire view.

packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx (3)

177-194: LGTM: Double-click handler updates.

The unconditional preventDefault() and stopPropagation() calls are appropriate for this interactive element to prevent text selection and event bubbling when double-clicking pieces.


568-593: LGTM: Key extraction and renderer updates.

Extracting elKey once and reusing it across all renderer components is cleaner than repeating unprotectString(piece.instance._id) multiple times.


604-618: LGTM: Drag state visual feedback integration.

The hasDraggableElement && dragCtx?.enabled condition correctly applies the draggable styling only when the piece supports retiming and edit mode is enabled.

Comment on lines 132 to 135
setTimeout(() => {
// after the timeout we want to bail out in case something went wrong
cleanup()
}, DRAG_TIMEOUT)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Timeout cleanup can interfere with subsequent drag operations.

The setTimeout at line 132-135 calls cleanup() unconditionally after 10 seconds. If the user completes a drag successfully and starts a new drag within that 10-second window, the timeout from the first drag will clear the state of the second drag.

Recommended fix: Track and clear the timeout
 	const startDrag = useCallback(
 		(
 			ogPiece: PieceUi,
 			timeScale: number,
 			pos: { x: number; y: number },
 			elementOffset?: number,
 			limitToPart?: PartInstanceId,
 			limitToSegment?: SegmentId
 		) => {
 			if (pieceId) return // a drag is currently in progress....

 			const inPoint = ogPiece.renderedInPoint ?? 0
 			segmentIdRef.current = limitToSegment
 			limitToPartRef.current = limitToPart
 			positionRef.current = pos
 			setPieceId(ogPiece.instance._id)

 			let localPiece = ogPiece
+			let timeoutId: ReturnType<typeof setTimeout> | undefined

 			const onMove = (e: MouseEvent) => {
 				// ... existing code
 			}

 			const onMouseUp = (e: MouseEvent) => {
 				// ... existing code
 			}

 			const cleanup = () => {
+				if (timeoutId) {
+					clearTimeout(timeoutId)
+					timeoutId = undefined
+				}
 				document.removeEventListener('mousemove', onMove)
 				document.removeEventListener('mouseup', onMouseUp)
 				setPieceId(undefined)
 				setPiece(undefined)
 				partIdRef.current = undefined
 				segmentIdRef.current = undefined
 			}

 			document.addEventListener('mousemove', onMove)
 			document.addEventListener('mouseup', onMouseUp)

-			setTimeout(() => {
+			timeoutId = setTimeout(() => {
 				cleanup()
 			}, DRAG_TIMEOUT)
 		},
 		[pieceId, t]
 	)
🤖 Prompt for AI Agents
In @packages/webui/src/client/ui/RundownView/DragContextProvider.tsx around
lines 132 - 135, The untracked setTimeout in DragContextProvider that calls
cleanup() after DRAG_TIMEOUT can clear state for subsequent drags; store the
timeout ID when you call setTimeout (e.g., local variable or ref like
dragTimeoutId) and clear it with clearTimeout(...) whenever a new drag starts or
when cleanup() runs (also set the ref to null after clearing) so the timeout for
an earlier drag cannot fire during a later drag; update the code paths around
setTimeout, cleanup(), and the drag-start logic to set/clear this tracked
timeout.

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

Comment on lines 30 to 33
/**
* Whether dragging is enabled
*/
enabled: boolean
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

const segmentIdRef = useRef<undefined | SegmentId>(undefined)
const limitToPartRef = useRef<undefined | PartInstanceId>(undefined)

const startDrag = (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines 76 to 77
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onMouseUp)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

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