Skip to content

feat: add a 'save' button in peripheral device settings, instead of autosave#1791

Open
jstarpl wants to merge 3 commits into
Sofie-Automation:mainfrom
nrkno:contrib/feat/peripheral-device-settings-save
Open

feat: add a 'save' button in peripheral device settings, instead of autosave#1791
jstarpl wants to merge 3 commits into
Sofie-Automation:mainfrom
nrkno:contrib/feat/peripheral-device-settings-save

Conversation

@jstarpl

@jstarpl jstarpl commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

About the Contributor

This pull request is posted on behalf of the NRK.

Type of Contribution

This is a:

Feature

Current Behavior

Peripheral device settings are autosaved on every input edit, potentially causing problems on air

New Behavior

Peripheral device settings are only saved upon clicking a "save" button

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 Peripheral Device configuration UI

Time Frame

Not urgent, but we would like to get this merged into the in-development release.

Other Information

This PR subsumes #1572 and adds some bugfixes/improvements. We like this feature and would like to see it merged, but the original PR has now become stale.

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.

@jstarpl jstarpl added the Contribution from NRK Contributions sponsored by NRK (nrk.no) label Jul 9, 2026
@jstarpl
jstarpl requested a review from nytamin July 9, 2026 12:19
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a staged "pending save" editing workflow to Studio device settings tables (Ingest, Input, Playout sub-devices, and the shared GenericSubDevicesTable) using local unsaved-overrides state, batched vs. instant override helpers, and updatedIds tracking with explicit Save/Discard controls. ParentDevices gains an analogous staged assignment flow. Unrelated Upgrades button restyling is included.

Changes

Staged Save/Discard editing workflow

Layer / File(s) Summary
Shared table editing contract and UI
packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx
Adds instantSaveOverrideHelper, hasUnsavedChanges, saveChanges, discardChanges, updateObjectId, updatedIds to props; deletion now commits via instant helper; SummaryRow shows "(pending save)"; edit row shows Save/Discard vs. finish button.
Ingest sub-devices staged overrides
packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx
Adds unsaved override state, batched/instant helpers, ID-update tracking, and save/discard persistence, wired into GenericSubDevicesTable.
Input sub-devices staged overrides
packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx
Adds unsaved override state, batched/instant helpers, ID-update tracking, and save/discard persistence, wired into GenericSubDevicesTable.
Playout sub-devices staged overrides
packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx
Adds unsaved override state, batched/instant helpers, ID-update tracking, and save/discard persistence, wired into GenericSubDevicesTable.
Parent devices staged assignment editing
packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx
Adds unsaved overrides/assignments state, changeAssignment/saveChanges/discardChanges handlers, and reworks ParentDeviceEditRow and AssignPeripheralDeviceConfigId to stage assignment changes with Save/Discard buttons instead of immediate backend calls.

Upgrades Fix Up buttons restyle

Layer / File(s) Summary
Button restyle
packages/webui/src/client/ui/Settings/Upgrades/Components.tsx
Reformats "Fix Up Config" and "Skip Fix Up Step" buttons and changes Bootstrap variant from light to outline-secondary.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SubDeviceEditRow
  participant GenericSubDevicesTable
  participant BatchedOverrideHelper
  participant StudioDocument

  User->>SubDeviceEditRow: edit device ID
  SubDeviceEditRow->>GenericSubDevicesTable: updateObjectId(oldId, newId)
  GenericSubDevicesTable->>BatchedOverrideHelper: changeItemId(...).commit()
  User->>GenericSubDevicesTable: click Save
  GenericSubDevicesTable->>StudioDocument: saveChanges() persists overrides
  StudioDocument-->>GenericSubDevicesTable: updatedIds cleared
Loading
sequenceDiagram
  participant User
  participant ParentDeviceEditRow
  participant StudioParentDevices
  participant Studios
  participant MeteorCall

  User->>ParentDeviceEditRow: select peripheral device
  ParentDeviceEditRow->>StudioParentDevices: changeAssignment(configId, deviceId)
  User->>ParentDeviceEditRow: click Save
  StudioParentDevices->>Studios: update(overrides)
  StudioParentDevices->>MeteorCall: assignConfigToPeripheralDevice(...)
  MeteorCall-->>StudioParentDevices: clear unsaved state
Loading

Suggested reviewers: nytamin, Julusian

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: switching peripheral device settings from autosave to a manual save flow.
Description check ✅ Passed The description matches the changeset and explains the move from autosave to a save-button workflow for peripheral device settings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx (1)

74-97: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Stage sub-device deletes instead of instant-saving them instantSaveOverrideHelper() here writes the current settingsWithOverrides array back to the DB, so a delete while other edits are staged persists those pending ops too and leaves the unsaved state able to restore the item on Save. Route this through overrideHelper() and drop the instant helper from this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx`
around lines 74 - 97, The sub-device removal flow in confirmRemove is using
instantSaveOverrideHelper(), which immediately writes the full settings state
and can accidentally persist other staged edits; switch this delete path to use
overrideHelper() so the removal is only staged, and remove the instant-save
helper from the useCallback dependencies and onAccept path.
packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx (1)

82-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stage the new ingest device instead of $pushing it directly. When unsavedOverrides is set, this bypasses the staged edit flow and the new item can stay hidden behind the pending overrides until save/discard. Also add studioId to the callback deps and drop settingsWithOverrides.overrides, which isn’t used here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx`
around lines 82 - 106, The addNewItem callback in IngestSubDevices is bypassing
the staged edit flow by calling Studios.update with a direct $push, which can
leave the new ingest device hidden when unsavedOverrides exists. Update the
logic to stage the new device through the same pending-override mechanism used
elsewhere in this component instead of writing directly, and ensure the callback
includes studioId in its dependency list while removing the unused
settingsWithOverrides.overrides dependency. Use addNewItem, Studios.update, and
wrappedSubDevices as the key locations to adjust.
🧹 Nitpick comments (2)
packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx (1)

300-309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

handleUpdateId omits editItemWithId from its dependency array.

It's called inside the callback but not listed in deps [item.id, updateObjectId]. If editItemWithId isn't stably memoized this captures a stale closure, and it will otherwise trip react-hooks/exhaustive-deps.

♻️ Proposed fix
-		[item.id, updateObjectId]
+		[item.id, updateObjectId, editItemWithId]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx`
around lines 300 - 309, The `handleUpdateId` callback in `GenericSubDevices` is
missing `editItemWithId` from its dependency array, which can leave a stale
closure and violate the hooks lint rule. Update the `useCallback` dependencies
for `handleUpdateId` to include every referenced value used inside the callback,
especially `editItemWithId`, alongside `item.id` and `updateObjectId`.
packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx (1)

126-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

saveChanges reads updatedIds but omits it from deps.

The dependency array is [studio?._id, unsavedOverrides]; updatedIds is used to decide the reset and can be stale. Add it (also satisfies exhaustive-deps).

♻️ Proposed fix
-	}, [studio?._id, unsavedOverrides])
+	}, [studio?._id, unsavedOverrides, updatedIds])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx`
around lines 126 - 139, The saveChanges callback in IngestSubDevices reads
updatedIds but does not list it in the useCallback dependency array, which can
leave the reset logic stale. Update the dependency list for saveChanges to
include updatedIds alongside studio?._id and unsavedOverrides so the callback
stays in sync and satisfies exhaustive-deps.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx`:
- Around line 335-341: The shared edit state in ParentDevices is being wired
into each row’s Save/Discard controls, so any expanded row can act on all
pending changes. Update the row rendering in ParentDevices/ParentDeviceRow usage
so Save/Discard are not repeated per row; either lift those actions out into a
single global toolbar for the whole table, or scope hasUnsavedChanges,
saveChanges, discardChanges, and currentAssignment to the specific row/item
being edited instead of using the shared unsavedOverrides/unsavedAssignments
state across all rows.

---

Outside diff comments:
In `@packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx`:
- Around line 74-97: The sub-device removal flow in confirmRemove is using
instantSaveOverrideHelper(), which immediately writes the full settings state
and can accidentally persist other staged edits; switch this delete path to use
overrideHelper() so the removal is only staged, and remove the instant-save
helper from the useCallback dependencies and onAccept path.

In `@packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx`:
- Around line 82-106: The addNewItem callback in IngestSubDevices is bypassing
the staged edit flow by calling Studios.update with a direct $push, which can
leave the new ingest device hidden when unsavedOverrides exists. Update the
logic to stage the new device through the same pending-override mechanism used
elsewhere in this component instead of writing directly, and ensure the callback
includes studioId in its dependency list while removing the unused
settingsWithOverrides.overrides dependency. Use addNewItem, Studios.update, and
wrappedSubDevices as the key locations to adjust.

---

Nitpick comments:
In `@packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx`:
- Around line 300-309: The `handleUpdateId` callback in `GenericSubDevices` is
missing `editItemWithId` from its dependency array, which can leave a stale
closure and violate the hooks lint rule. Update the `useCallback` dependencies
for `handleUpdateId` to include every referenced value used inside the callback,
especially `editItemWithId`, alongside `item.id` and `updateObjectId`.

In `@packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx`:
- Around line 126-139: The saveChanges callback in IngestSubDevices reads
updatedIds but does not list it in the useCallback dependency array, which can
leave the reset logic stale. Update the dependency list for saveChanges to
include updatedIds alongside studio?._id and unsavedOverrides so the callback
stays in sync and satisfies exhaustive-deps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c94d86ca-fa6d-4518-912f-7c8aecb1a8cd

📥 Commits

Reviewing files that changed from the base of the PR and between 10db30e and 5569c98.

📒 Files selected for processing (6)
  • packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx
  • packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx
  • packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx
  • packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx
  • packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx
  • packages/webui/src/client/ui/Settings/Upgrades/Components.tsx

Comment on lines +335 to +341
hasUnsavedChanges={hasUnsavedChanges}
saveChanges={saveChanges}
discardChanges={discardChanges}
currentAssignment={
item.id in unsavedAssignments ? unsavedAssignments[item.id] : peripheralDevice?._id
}
changeAssignment={changeAssignment}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and surrounding symbols.
ast-grep outline packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx --view expanded || true

# Read the relevant sections around the reported lines and the helper functions.
sed -n '120,220p' packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx | cat -n
printf '\n----\n'
sed -n '300,380p' packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx | cat -n
printf '\n----\n'
sed -n '500,560p' packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx | cat -n

# Find all uses of hasUnsavedChanges, discardChanges, saveChanges, and ParentDeviceEditRow.
rg -n "hasUnsavedChanges|discardChanges|saveChanges|ParentDeviceEditRow" packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx

Repository: Sofie-Automation/sofie-core

Length of output: 13511


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the save/discard implementation and nearby state.
sed -n '84,116p' packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx | cat -n
printf '\n----\n'
sed -n '116,160p' packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx | cat -n

# Locate the expansion helper definition.
rg -n "function useToggleExpandHelper|const useToggleExpandHelper|useToggleExpandHelper" packages/webui/src/client/ui -g '!**/dist/**' -g '!**/build/**'

Repository: Sofie-Automation/sofie-core

Length of output: 6246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' packages/webui/src/client/ui/util/useToggleExpandHelper.tsx | cat -n

Repository: Sofie-Automation/sofie-core

Length of output: 1627


Avoid repeating shared Save/Discard controls per row

hasUnsavedChanges is shared across the whole table, so any expanded row shows Save/Discard as soon as one edit exists. Since discardChanges clears both unsavedOverrides and unsavedAssignments, clicking Discard in any row drops edits from every row, not just the one being edited. If this is meant to be one global edit session, move the actions out of each row; otherwise scope dirty state and discard to the current row.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx`
around lines 335 - 341, The shared edit state in ParentDevices is being wired
into each row’s Save/Discard controls, so any expanded row can act on all
pending changes. Update the row rendering in ParentDevices/ParentDeviceRow usage
so Save/Discard are not repeated per row; either lift those actions out into a
single global toolbar for the whole table, or scope hasUnsavedChanges,
saveChanges, discardChanges, and currentAssignment to the specific row/item
being edited instead of using the shared unsavedOverrides/unsavedAssignments
state across all rows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Contribution from NRK Contributions sponsored by NRK (nrk.no)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants