Skip to content

Conversation

@anteeek
Copy link

@anteeek anteeek commented Dec 9, 2025

About the Contributor

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

Peripheral device settings UI

Time Frame

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

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 PR replaces autosave behavior in peripheral device settings with an explicit save workflow. Instead of persisting changes immediately on each input edit, users must now click a "save" button to commit configuration changes, preventing accidental on-air modifications.

Changes Overview

The implementation introduces a consistent staged-changes pattern across five peripheral device setting components. Each component now maintains local state for unsaved changes before persisting to the database.

Core Architecture Changes

Staged Override Management:

  • Introduces unsavedOverrides state in each device component (Ingest, Input, Playout, Parent) to batch configuration changes
  • Derives settingsWithOverrides by merging base settings with unsaved changes, ensuring UI reflects pending modifications
  • Replaces direct studio updates with batched override helpers for in-memory staging

Change Tracking:

  • Adds updatedIds map to track device ID renames during edits
  • Provides updateObjectId handler to apply ID changes through batched helpers
  • Derives hasUnsavedChanges flag from unsaved overrides and updated IDs

Persistence Control:

  • Implements saveChanges handler to persist all staged changes to the database in a single operation
  • Implements discardChanges handler to revert all unsaved modifications
  • Clears unsaved state after successful save

Component-Level Changes

GenericSubDevices.tsx: Extended public interfaces and component props:

  • SubDevicesTableProps, SummaryRowProps, SubDeviceEditRowProps now include change-tracking properties
  • Added: instantSaveOverrideHelper, hasUnsavedChanges, saveChanges, discardChanges, updateObjectId, updatedIds
  • SummaryRow displays pending-save indicator when device ID has changed
  • SubDeviceEditRow conditionally renders Discard/Save buttons when unsaved changes exist; otherwise shows original Confirm flow

IngestSubDevices.tsx (+63/-10):

  • Introduces unsavedOverrides state and baseSettings memo
  • Derives settingsWithOverrides for staged changes
  • Propagates new change-tracking props to GenericSubDevicesTable

InputSubDevices.tsx (+65/-10):

  • Similar staged override implementation with unsavedOverrides and settingsWithOverrides
  • Recomputes wrappedSubDevices based on staged settings
  • Extends addNewItem logic to work with staged changes

ParentDevices.tsx (+107/-40):

  • Tracks unsaved device config assignments alongside overrides
  • AssignPeripheralDeviceConfigId refactored: removed studioId parameter, added onChange callback for staged updates
  • New props in GenericParentDevicesTableProps and ParentDeviceEditRowProps for assignment state management

PlayoutSubDevices.tsx (+60/-5):

  • Implements unsavedOverrides and updatedIds tracking
  • Derives deviceSettings conditionally from unsaved or base settings
  • Passes control props to GenericSubDevicesTable

Benefits

  • Prevents accidental on-air configuration changes from unintended edits
  • Allows users to review all pending changes before committing
  • Enables atomic persistence of related configuration changes
  • Reduces risk of partial or concurrent edits affecting live systems

@codecov-commenter
Copy link

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)
33.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@Saftret Saftret added the Contribution from SuperFly.tv Contributions sponsored by SuperFly.tv label Dec 10, 2025
@anteeek anteeek marked this pull request as ready for review December 16, 2025 09:18
@anteeek anteeek requested a review from a team as a code owner December 16, 2025 09:18
@anteeek anteeek changed the title SOFIE-40 | add a 'save' button in peripheral device settings, instead… feat: add a 'save' button in peripheral device settings, instead of autosave Jan 12, 2026
@nytamin nytamin requested a review from tsorbo January 12, 2026 10:02
@jstarpl
Copy link
Contributor

jstarpl commented Jan 12, 2026

@coderabbitai review

@coderabbitai
Copy link

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 12, 2026

Walkthrough

The pull request introduces a batched override management system across device settings components. Changes enable users to make multiple edits to device configurations before committing to the backend, with state tracking for unsaved overrides and ID changes, plus save/discard handlers throughout the component hierarchy.

Changes

Cohort / File(s) Summary
Core Component Extension
packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx
Extended SubDevicesTableProps, SummaryRowProps, and SubDeviceEditRowProps interfaces with unsaved changes tracking props: instantSaveOverrideHelper, hasUnsavedChanges, saveChanges, discardChanges, updateObjectId, updatedIds. Added pending-save indicator rendering and reworked edit action controls to show Save/Discard buttons when unsaved changes exist.
Sub-Device Batched Overrides
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/PlayoutSubDevices.tsx
Introduced local unsavedOverrides state and updatedIds Map to batch changes before persistence. Replaced direct override handling with batchedOverrideHelper and added instantSaveOverrideHelper. Derived settingsWithOverrides to layer staged changes. Implemented saveChanges and discardChanges handlers; propagated new props to GenericSubDevicesTable.
Parent Device Assignment Tracking
packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx
Introduced unsaved overrides and per-config device assignment state. Replaced direct database updates with centralized handlers; refactored AssignPeripheralDeviceConfigId to use callback-based updates. Extended GenericParentDevicesTableProps, ParentDevicesTableProps, ParentDeviceEditRowProps, and AssignPeripheralDeviceConfigIdProps with hasUnsavedChanges, saveChanges, discardChanges, and assignment tracking props.
Package Dependencies
package.json
Updated dependency versions and build configuration (+61/-14 lines).

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as Edit UI Component
    participant State as Local State Manager
    participant Backend as Database/Backend

    User->>UI: Make changes (e.g., edit ID, settings)
    activate UI
    UI->>State: Update unsavedOverrides via batchedOverrideHelper
    State->>State: Store staged changes in memory
    State->>UI: Reflect updates (show Save/Discard buttons)
    deactivate UI
    
    User->>UI: Click Save or Discard
    activate UI
    alt Save Changes
        UI->>State: Invoke saveChanges()
        State->>Backend: Persist unsavedOverrides via Studios.update/MeteorCall
        Backend->>Backend: Commit changes to database
        State->>State: Clear unsavedOverrides & updatedIds
        State->>UI: Update UI (hide Save/Discard, show edit controls)
    else Discard Changes
        UI->>State: Invoke discardChanges()
        State->>State: Reset unsavedOverrides & updatedIds to empty
        State->>UI: Revert UI to last-saved state
    end
    deactivate UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes


Poem

🐰 Hops excitedly

Once scattered, now batched—changes bloom unsaved,
A rabbit's delight: edit, then decide!
Save or discard with a flick of the ear,
No instant commits when mistakes appear.
Staged edits hop closer to perfection's care!

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: replacing autosave with a manual save button for peripheral device settings.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

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: 5

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/InputSubDevices.tsx (1)

76-100: Missing studioId in dependency array.

The addNewItem callback uses studioId in the Studios.update call but it's not included in the dependency array. Additionally, settingsWithOverrides.overrides is in the dependency array but isn't used in the callback.

Proposed fix
 	const addNewItem = useCallback(() => {
 		const existingDevices = new Set(wrappedSubDevices.map((d) => d.id))
 		let idx = 0
 		while (existingDevices.has(`device${idx}`)) {
 			idx++
 		}
 
 		const newId = `device${idx}`
 		const newDevice = literal<StudioInputDevice>({
 			peripheralDeviceId: undefined,
 			options: {},
 		})
 
 		const addOp = literal<ObjectOverrideSetOp>({
 			op: 'set',
 			path: newId,
 			value: newDevice,
 		})
 
 		Studios.update(studioId, {
 			$push: {
 				'peripheralDeviceSettings.inputDevices.overrides': addOp,
 			},
 		})
-	}, [wrappedSubDevices, settingsWithOverrides.overrides])
+	}, [studioId, wrappedSubDevices])
packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx (1)

79-103: Missing studioId in dependency array.

Same issue as in InputSubDevices.tsx — the callback uses studioId but it's not in the dependency array.

Proposed fix
-	}, [wrappedSubDevices, settingsWithOverrides.overrides])
+	}, [studioId, wrappedSubDevices])
🤖 Fix all issues with AI agents
In @packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx:
- Around line 300-309: handleUpdateId captures editItemWithId but the hook
dependency array only lists [item.id, updateObjectId], risking stale closures;
update the useCallback dependencies to include editItemWithId (i.e., change the
dependency array for handleUpdateId to [item.id, updateObjectId,
editItemWithId]) so the callback updates when editItemWithId changes.

In @packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx:
- Around line 122-135: The saveChanges callback is missing updatedIds in its
dependency array, which can cause stale closures; update the useCallback
declaration for saveChanges to include updatedIds (and setUpdatedIds if not
already stable) in its dependency array so the hook rebinds when updatedIds
changes; locate the saveChanges function and modify its dependency list to
include updatedIds (and ensure setUpdatedIds is referenced correctly) so the
reset logic using setUpdatedIds(new Map<string, string>()) runs with the latest
state.
- Line 153: Compute and pass a memoized hasUnsavedChanges that includes both
unsavedOverrides and updatedIds like the other components: create a useMemo that
returns (!!unsavedOverrides || updatedIds.size > 0) and use that value for the
hasUnsavedChanges prop instead of the current !!unsavedOverrides; reference the
existing updatedIds and unsavedOverrides variables and the hasUnsavedChanges
prop in the component so Save/Discard appear when an ID was renamed.

In @packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx:
- Around line 119-132: The saveChanges callback references updatedIds.size but
updatedIds is not included in its dependency array, which can cause stale reads;
update the dependency array for saveChanges to include updatedIds (and keep
setUpdatedIds and other existing deps like studio?._id and unsavedOverrides) so
the hook correctly re-registers when updatedIds changes; locate the saveChanges
function and add updatedIds to its useCallback dependency list.

In @packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx:
- Around line 125-138: The saveChanges callback uses updatedIds.size but
updatedIds is not included in the dependency array, which can produce a stale
closure; update the dependency array for saveChanges to include updatedIds (and
keep studio?._id and unsavedOverrides) so the callback sees the current Map,
ensuring the branch that checks updatedIds.size runs correctly and that
setUpdatedIds(new Map<string,string>()) resets state as intended; locate the
saveChanges function and modify its dependency array to include updatedIds.
🧹 Nitpick comments (1)
packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx (1)

114-123: Consider surfacing assignment save failures to the user.

The Promise.all(...).catch() only logs to console.error. If any assignment fails, the user won't receive feedback, and unsavedAssignments is cleared regardless of success or failure. Consider adding user-facing error notification.

Proposed improvement sketch
 		if (Object.keys(unsavedAssignments).length > 0) {
 			Promise.all(
 				Object.entries<PeripheralDeviceId | undefined>(unsavedAssignments).map(async ([configId, deviceId]) => {
 					return MeteorCall.studio.assignConfigToPeripheralDevice(studioId, configId, deviceId ?? null)
 				})
-			).catch((e) => {
+			).then(() => {
+				setUnsavedAssignments({})
+			}).catch((e) => {
 				console.error('Failed to save assignments', e)
+				// TODO: Show user-facing error notification
 			})
-			setUnsavedAssignments({})
 		}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24b7c79 and 2f87e4f.

📒 Files selected for processing (5)
  • 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
🧰 Additional context used
🧬 Code graph analysis (5)
packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx (3)
packages/corelib/src/settings/objectWithOverrides.ts (2)
  • SomeObjectOverrideOp (19-19)
  • wrapDefaultObject (48-53)
packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx (1)
  • useOverrideOpHelper (14-29)
packages/corelib/src/dataModel/Studio.ts (1)
  • StudioInputDevice (113-121)
packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx (3)
packages/corelib/src/settings/objectWithOverrides.ts (2)
  • SomeObjectOverrideOp (19-19)
  • wrapDefaultObject (48-53)
packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx (1)
  • useOverrideOpHelper (14-29)
packages/corelib/src/dataModel/Studio.ts (1)
  • StudioIngestDevice (103-111)
packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx (3)
packages/corelib/src/settings/objectWithOverrides.ts (1)
  • SomeObjectOverrideOp (19-19)
packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx (1)
  • useOverrideOpHelper (14-29)
packages/corelib/src/dataModel/Studio.ts (1)
  • StudioPlayoutDevice (123-131)
packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx (2)
packages/corelib/src/overrideOpHelper.ts (1)
  • OverrideOpHelper (174-174)
packages/webui/src/client/lib/Components/TextInput.tsx (1)
  • TextInputControl (27-121)
packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx (4)
packages/corelib/src/settings/objectWithOverrides.ts (2)
  • SomeObjectOverrideOp (19-19)
  • wrapDefaultObject (48-53)
packages/corelib/src/dataModel/Studio.ts (1)
  • StudioDeviceSettings (133-140)
packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx (1)
  • useOverrideOpHelper (14-29)
packages/webui/src/client/lib/Components/DropdownInput.tsx (1)
  • DropdownInputOption (5-9)
🔇 Additional comments (4)
packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx (1)

344-360: LGTM!

The conditional rendering for Save/Discard buttons vs. the Confirm button is well structured. The UI correctly surfaces unsaved changes state and provides clear action buttons.

packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx (1)

53-73: LGTM!

The batched override pattern is correctly implemented. The deviceSettings memo properly layers unsavedOverrides on top of baseSettings, and wrappedSubDevices correctly depends on the merged settings.

packages/webui/src/client/ui/Settings/Studio/Devices/ParentDevices.tsx (2)

47-62: LGTM!

The state management for unsavedOverrides and unsavedAssignments is well structured. The deviceSettings memo correctly merges unsaved changes with the base settings.


491-507: LGTM!

The conditional Save/Discard UI in ParentDeviceEditRow follows the same pattern as the other device edit rows and provides a consistent user experience.

Comment on lines +300 to 309
const handleUpdateId = useCallback(
(newId: string) => {
if (item.id === newId) return

overrideHelper().changeItemId(item.id, newId).commit()
updateObjectId(item.id, newId)

// toggle ui visibility
editItemWithId(item.id, false)
editItemWithId(newId, true)
},
[item.id, overrideHelper, editItemWithId]
[item.id, updateObjectId]
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing editItemWithId in dependency array.

The handleUpdateId callback uses editItemWithId but it's not included in the dependency array. This can cause stale closure issues if editItemWithId changes.

Proposed fix
 	const handleUpdateId = useCallback(
 		(newId: string) => {
 			updateObjectId(item.id, newId)
 
 			// toggle ui visibility
 			editItemWithId(item.id, false)
 			editItemWithId(newId, true)
 		},
-		[item.id, updateObjectId]
+		[item.id, updateObjectId, editItemWithId]
 	)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleUpdateId = useCallback(
(newId: string) => {
if (item.id === newId) return
overrideHelper().changeItemId(item.id, newId).commit()
updateObjectId(item.id, newId)
// toggle ui visibility
editItemWithId(item.id, false)
editItemWithId(newId, true)
},
[item.id, overrideHelper, editItemWithId]
[item.id, updateObjectId]
)
const handleUpdateId = useCallback(
(newId: string) => {
updateObjectId(item.id, newId)
// toggle ui visibility
editItemWithId(item.id, false)
editItemWithId(newId, true)
},
[item.id, updateObjectId, editItemWithId]
)
🤖 Prompt for AI Agents
In @packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx
around lines 300 - 309, handleUpdateId captures editItemWithId but the hook
dependency array only lists [item.id, updateObjectId], risking stale closures;
update the useCallback dependencies to include editItemWithId (i.e., change the
dependency array for handleUpdateId to [item.id, updateObjectId,
editItemWithId]) so the callback updates when editItemWithId changes.

Comment on lines +122 to +135
const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.ingestDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}

if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing updatedIds in dependency array.

Same issue as the other sub-device files.

Proposed fix
-	}, [studio?._id, unsavedOverrides])
+	}, [studio?._id, unsavedOverrides, updatedIds])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.ingestDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}
if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides])
const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.ingestDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}
if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides, updatedIds])
🤖 Prompt for AI Agents
In @packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx
around lines 122 - 135, The saveChanges callback is missing updatedIds in its
dependency array, which can cause stale closures; update the useCallback
declaration for saveChanges to include updatedIds (and setUpdatedIds if not
already stable) in its dependency array so the hook rebinds when updatedIds
changes; locate the saveChanges function and modify its dependency list to
include updatedIds (and ensure setUpdatedIds is referenced correctly) so the
reset logic using setUpdatedIds(new Map<string, string>()) runs with the latest
state.

overrideHelper={overrideHelper}
overrideHelper={batchedOverrideHelper}
peripheralDevices={filteredPeripheralDevices}
hasUnsavedChanges={!!unsavedOverrides}
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

Inconsistent hasUnsavedChanges computation — missing updatedIds check.

Unlike PlayoutSubDevices.tsx and InputSubDevices.tsx which compute hasUnsavedChanges as !!unsavedOverrides || updatedIds.size > 0, this file only checks !!unsavedOverrides. If a user renames a device ID but makes no other changes, the Save/Discard buttons won't appear.

Proposed fix

Add a memoized hasUnsavedChanges computation consistent with the other files:

+	const hasUnsavedChanges = useMemo(() => !!unsavedOverrides || updatedIds.size > 0, [unsavedOverrides, updatedIds])
+
 	return (
 		<div className="mb-4">
 			...
 			<GenericSubDevicesTable
 				...
-				hasUnsavedChanges={!!unsavedOverrides}
+				hasUnsavedChanges={hasUnsavedChanges}
 				...
 			/>
🤖 Prompt for AI Agents
In @packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx at
line 153, Compute and pass a memoized hasUnsavedChanges that includes both
unsavedOverrides and updatedIds like the other components: create a useMemo that
returns (!!unsavedOverrides || updatedIds.size > 0) and use that value for the
hasUnsavedChanges prop instead of the current !!unsavedOverrides; reference the
existing updatedIds and unsavedOverrides variables and the hasUnsavedChanges
prop in the component so Save/Discard appear when an ID was renamed.

Comment on lines +119 to +132
const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.inputDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}

if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing updatedIds in dependency array.

Same issue as in PlayoutSubDevices.tsx - the callback reads updatedIds.size but updatedIds is not in the dependency array.

Proposed fix
 	const saveChanges = useCallback(() => {
 		if (studio?._id && unsavedOverrides) {
 			Studios.update(studio._id, {
 				$set: {
 					'peripheralDeviceSettings.inputDevices.overrides': unsavedOverrides,
 				},
 			})
 			setUnsavedOverrides(undefined)
 		}
 
 		if (updatedIds.size > 0) {
 			setUpdatedIds(new Map<string, string>())
 		}
-	}, [studio?._id, unsavedOverrides])
+	}, [studio?._id, unsavedOverrides, updatedIds])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.inputDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}
if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides])
const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.inputDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}
if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides, updatedIds])
🤖 Prompt for AI Agents
In @packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx
around lines 119 - 132, The saveChanges callback references updatedIds.size but
updatedIds is not included in its dependency array, which can cause stale reads;
update the dependency array for saveChanges to include updatedIds (and keep
setUpdatedIds and other existing deps like studio?._id and unsavedOverrides) so
the hook correctly re-registers when updatedIds changes; locate the saveChanges
function and add updatedIds to its useCallback dependency list.

Comment on lines +125 to +138
const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.playoutDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}

if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing updatedIds in dependency array.

The saveChanges callback reads updatedIds.size but updatedIds is not in the dependency array. This can lead to stale closure issues.

Proposed fix
 	const saveChanges = useCallback(() => {
 		if (studio?._id && unsavedOverrides) {
 			Studios.update(studio._id, {
 				$set: {
 					'peripheralDeviceSettings.playoutDevices.overrides': unsavedOverrides,
 				},
 			})
 			setUnsavedOverrides(undefined)
 		}
 
 		if (updatedIds.size > 0) {
 			setUpdatedIds(new Map<string, string>())
 		}
-	}, [studio?._id, unsavedOverrides])
+	}, [studio?._id, unsavedOverrides, updatedIds])
🤖 Prompt for AI Agents
In @packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx
around lines 125 - 138, The saveChanges callback uses updatedIds.size but
updatedIds is not included in the dependency array, which can produce a stale
closure; update the dependency array for saveChanges to include updatedIds (and
keep studio?._id and unsavedOverrides) so the callback sees the current Map,
ensuring the branch that checks updatedIds.size runs correctly and that
setUpdatedIds(new Map<string,string>()) resets state as intended; locate the
saveChanges function and modify its dependency array to include updatedIds.

Copy link
Contributor

@tsorbo tsorbo left a comment

Choose a reason for hiding this comment

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

I have not review the code, but Henrik and I at NRK like the idea of adding a save button when editing peripheral device settings.

Copy link
Contributor

@jstarpl jstarpl left a comment

Choose a reason for hiding this comment

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

Other than the things coderabbit pointed out, I have these suggestions:

}, [studioId, wrappedSubDevices])
}, [wrappedSubDevices, settingsWithOverrides.overrides])

const [updatedIds, setUpdatedIds] = useState(new Map<string, string>())
Copy link
Contributor

Choose a reason for hiding this comment

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

I would really appreciate a comment here describing what the strings in key and value of this Map actually mean, that the key is the oldId for a sub-device, and the value is the newId.

Comment on lines +102 to +134
const [updatedIds, setUpdatedIds] = useState(new Map<string, string>())

const updateObjectId = useCallback(
(oldId: string, newId: string) => {
if (oldId === newId) return

batchedOverrideHelper().changeItemId(oldId, newId).commit()
setUpdatedIds((prev) => new Map(prev).set(oldId, newId))
},
[batchedOverrideHelper, setUpdatedIds]
)

const discardChanges = useCallback(() => {
setUnsavedOverrides(undefined)
setUpdatedIds(new Map<string, string>())
}, [])

const saveChanges = useCallback(() => {
if (studio?._id && unsavedOverrides) {
Studios.update(studio._id, {
$set: {
'peripheralDeviceSettings.inputDevices.overrides': unsavedOverrides,
},
})
setUnsavedOverrides(undefined)
}

if (updatedIds.size > 0) {
setUpdatedIds(new Map<string, string>())
}
}, [studio?._id, unsavedOverrides])

const hasUnsavedChanges = useMemo(() => !!unsavedOverrides || updatedIds.size > 0, [unsavedOverrides, updatedIds])
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.

This whole section seems to be repeated for all of these *Devices.tsx files. Do you think it would make sense to maybe wrap them in a React hook that takes the one variable part (I think the saveChanges callback, perhaps even just the key for the $set object?), and just warp it up in a reusable code block?

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

Labels

Contribution from SuperFly.tv Contributions sponsored by SuperFly.tv

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants