Attempt at multi-edit#1994
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…s, fixing message for automation section
|
Thank you for this @cdslipp There seems to be several unrelated changes which we would be inclined to removing (even if they are good) to keep this change clean The entry merging is slightly complex and we can probably simplify by leveraging some of the existing processes or pushing logic into the server. One first step is to check if the batch endpoint can simplify things I also wonder if we may be better off not using a new multimerge panel but leverage the existing. Since the components are modified to accept indeterminate states, we could generalise the logic It could also be worth it to revisit some of the old attempts. The performance issue was hopefully mitigated by the recent changes and we can see which decisions we can recover from past efforts I wil take some time to go through the changes more carefully and give a proper code review. |
|
@coderabbitai full review |
There was a problem hiding this comment.
Pull request overview
Adds a dedicated multi-event (“batch edit”) editor UI to the rundown entry editor, enabling editing of shared event fields with mixed/indeterminate states.
Changes:
- Introduces
MultiEventEditorUI and supporting merge logic for multi-selection editing. - Updates selection store behavior to avoid mutating
Setinstances in Zustand state. - Extends shared input components (Select, Switch, TextArea, SwatchSelect, TimeInput) to better support mixed/empty states in multi-edit.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/client/src/features/rundown/useEventSelection.ts | Avoids in-place Set mutation to ensure selection updates trigger renders correctly. |
| apps/client/src/features/rundown/entry-editor/multi-edit/useMultiEventMerge.ts | Adds hook to merge selected events into a “multi-edit view model”. |
| apps/client/src/features/rundown/entry-editor/multi-edit/multiEditUtils.ts | Implements merge logic and indeterminate handling for multi-editable fields. |
| apps/client/src/features/rundown/entry-editor/multi-edit/MultiEventEditor.tsx | New batch editor UI for multi-selected events. |
| apps/client/src/features/rundown/entry-editor/multi-edit/MultiEventEditor.module.scss | Styles for the new multi-event editor. |
| apps/client/src/features/rundown/entry-editor/composite/EventTextArea.tsx | Adds optional placeholder support (needed for “multiple” state). |
| apps/client/src/features/rundown/entry-editor/RundownEntryEditor.tsx | Switches to MultiEventEditor when more than one event is selected. |
| apps/client/src/features/rundown/_blockMixins.scss | Changes selected outline color (now matches cursor color). |
| apps/client/src/common/components/switch/Switch.tsx | Adds indeterminate prop and exposes it via a data attribute. |
| apps/client/src/common/components/switch/Switch.module.scss | Hides thumb when data-indeterminate is set. |
| apps/client/src/common/components/select/Select.tsx | Adds placeholder rendering support (used for “Mixed”). |
| apps/client/src/common/components/input/time-input/TimeInput.tsx | Shows blank input for undefined/NaN times (supports multi-edit indeterminate). |
| apps/client/src/common/components/input/textarea/Textarea.module.scss | Styles placeholders as italic. |
| apps/client/src/common/components/input/input/Input.module.scss | Styles placeholders as italic. |
| apps/client/src/common/components/input/colour-input/SwatchSelect.tsx | Adds noSelection to represent mixed colour state. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| custom: { ...first.custom }, | ||
| allLockDuration: false, | ||
| allLockEnd: false, | ||
| }; | ||
|
|
||
| // If the first event in the merged set is the first rundown event, seed linkStart from the second event | ||
| const firstIsRundownFirst = first.id === firstRundownEventId; | ||
| if (firstIsRundownFirst && events.length >= 2) { | ||
| merged.linkStart = events[1].linkStart; | ||
| } | ||
|
|
||
| for (let i = 1; i < events.length; i++) { | ||
| const event = events[i]; | ||
| for (const field of mergeableFields) { | ||
| if (field === 'linkStart') continue; // handled separately below | ||
| if (merged[field] !== INDETERMINATE && event[field] !== first[field]) { | ||
| (merged[field] as MergedValue<unknown>) = INDETERMINATE; | ||
| } | ||
| } | ||
| // Merge custom fields per-key | ||
| for (const key of Object.keys(merged.custom)) { | ||
| if (merged.custom[key] !== INDETERMINATE && event.custom[key] !== first.custom[key]) { | ||
| merged.custom[key] = INDETERMINATE; | ||
| } | ||
| } |
There was a problem hiding this comment.
mergeEvents() seeds merged.custom from the first selected event and then only iterates Object.keys(merged.custom). If different selected events have different custom-field keys (or missing keys), those keys are ignored in the merge and the UI can show an incorrect mixed/blank state. Consider merging over the union of custom-field keys across all selected events (or better: over the CustomFields schema keys) and comparing values using the same fallback as the single editor (event.custom[key] ?? '').
| $block-box-shadow: rgba(0, 0, 0, 0.5) 0 0 3px 2px; | ||
| $secondary-block-height: 2.5rem; | ||
| $block-selected-color: $blue-400; | ||
| $block-selected-color: $orange-400; |
There was a problem hiding this comment.
$block-selected-color is now the same value as $block-cursor-color ($orange-400). Since .selected and .hasCursor both use a 1px outline, this makes cursor vs selection visually indistinguishable and removes state information from the UI. Consider keeping these colors distinct (or otherwise differentiating the styles).
| $block-selected-color: $orange-400; | |
| $block-selected-color: $orange-300; |
| const resetValue = useCallback(() => { | ||
| if (typeof time !== 'number' || isNaN(time)) { | ||
| setValue('00:00:00'); | ||
| setValue(''); |
There was a problem hiding this comment.
question: can you explain why do we need this change?
There was a problem hiding this comment.
This was my workaround to getting empty state in the multi editor (ie when showing start and end time I want them to always be blank to imply there is a mixed value). However, there might be a clear way to do this so that we don't affect other areas of the app? I will try to make this change closer to the actual editor so its not a default state
| &[data-indeterminate] { | ||
| opacity: 0; | ||
| } |
There was a problem hiding this comment.
question: the switch without the indicator looks broken. indeterminate switches are strange and was one of the reasons we considered using another design element
to keep things simple here, lets stick with switches but improve the design, maybe there are relevant UX resources for this subject? I saw some that had a line in the middle of the switch which should be good enough if we dont have better ideas
|
|
||
| interface SwitchProps extends BaseSwitch.Root.Props { | ||
| size?: 'medium' | 'large'; | ||
| indeterminate?: boolean; |
There was a problem hiding this comment.
question: what is the relationship between value and indeterminate?
Would we get easier logic by extending value to be something like
value: boolean | 'indeterminate'
There was a problem hiding this comment.
I was literally just using this so we have a separate state ie that we can always toggle a mixed switch to 'ON'. However as I have been thinking about this and trying to rationalize... I realized its not computationally heavy at all to figure out the majority state - ie if most of the switches the selection are off, why not always make it so the toggle goes towards the majority first? If most switches in the selection are off, switch off. If most switches are on, switch on... this will make this feel more logical and not just like a random choice
| allLockEnd: boolean; | ||
| }; | ||
|
|
||
| export function mergeEvents(entries: RundownEntries, selectedIds: Set<string>, flatOrder: EntryId[]): MergedEvent | null { |
There was a problem hiding this comment.
question: instead of merging all events, how about merging each entry values on its own?
this means that we will have more but simpler logic, the logic will also be closer to the consumer and we will have better opportunity to deal with the edge cases of each value
| // if event is already selected, we remove it from selection | ||
| // and set the anchor to the event after | ||
| selectedEvents.delete(id); | ||
| const remaining = new Set(selectedEvents); |
There was a problem hiding this comment.
question: can you explain this change?
There was a problem hiding this comment.
I was trying to understand Zustand as I haven't worked much with it before... as far I as I can tell with zustand + react most states are immutable so you have to create a new reference object instead of trying to mutate the original for it to be truly reactive? Pretty sure I got here after it not reactively updating the first time. I normally work with Svelte now, so I'm not really used to this kind of thing, so please let me know if there is a cleaner approach here!
https://zustand.docs.pmnd.rs/learn/guides/immutable-state-and-merging#immutable-state-and-merging
| allLockEnd: boolean; | ||
| }; | ||
|
|
||
| export function mergeEvents(entries: RundownEntries, selectedIds: Set<string>, flatOrder: EntryId[]): MergedEvent | null { |
There was a problem hiding this comment.
suggestion: I think it is great that we push the business logic away into separate modules
try and keep the business logic pure (no side effects) and make a test suite to validate it

Demo Video
I need to clean up the code, I am touching too many existing components and I am going to break the main MultiEventEditor.tsx into smaller components to mirror the main EventEditor components. Either way please let me know what you think of the functionality and approach! This PR is a bit bigger than I wanted, but I couldnt figure out how to integrate with the existing editor component without overcomplicating stuff like crazy. A completely separate UI component makes more sense to me, but it is one more thing to maintain which is annoying.
List of Batch Edit Behaviours and Mixed States
Right now, I have been messing with multiple UI states for each selection in the editor, here are my current choices:
Event Schedule
-If end lock is mixed or off, clicking the lock will enable duration lock for all, but this presents a dialog to the user to confirm
Event Behaviour
Event Data
Custom Fields
If there are no custom fields, show the manage custom fields link same as single select
Automations
I looked at this briefly and it feels too complex right now because there is no easy batch endpoint for triggers/automations... so as far as I can tell this would be clunky to set up without setting up a server route. This could be an improvement later one