Skip to content

Commit 2f87e4f

Browse files
committed
SOFIE-40 | add a 'save' button in peripheral device settings, instead of autosave
1 parent 24b7c79 commit 2f87e4f

File tree

5 files changed

+356
-79
lines changed

5 files changed

+356
-79
lines changed

packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
WrappedOverridableItemDeleted,
88
WrappedOverridableItemNormal,
99
} from '../../util/OverrideOpHelper.js'
10-
import { faCheck, faPencilAlt, faSync, faTrash } from '@fortawesome/free-solid-svg-icons'
10+
import { faCheck, faPencilAlt, faSync, faTrash, faSave, faBan } from '@fortawesome/free-solid-svg-icons'
1111
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
1212
import { JSONBlob, JSONBlobParse, JSONSchema } from '@sofie-automation/blueprints-integration'
1313
import { DropdownInputControl, DropdownInputOption } from '../../../../lib/Components/DropdownInput.js'
@@ -32,12 +32,24 @@ interface PeripheralDeviceTranslated {
3232
export interface SubDevicesTableProps {
3333
subDevices: WrappedOverridableItem<any>[]
3434
overrideHelper: OverrideOpHelper
35+
instantSaveOverrideHelper: OverrideOpHelper
3536
peripheralDevices: PeripheralDevice[]
37+
hasUnsavedChanges: boolean
38+
saveChanges: () => void
39+
discardChanges: () => void
40+
updateObjectId: (oldId: string, newId: string) => void
41+
updatedIds: Map<string, string>
3642
}
3743
export function GenericSubDevicesTable({
3844
subDevices,
3945
overrideHelper,
4046
peripheralDevices,
47+
hasUnsavedChanges,
48+
instantSaveOverrideHelper,
49+
saveChanges,
50+
discardChanges,
51+
updateObjectId,
52+
updatedIds,
4153
}: Readonly<SubDevicesTableProps>): JSX.Element {
4254
const { t } = useTranslation()
4355
const { toggleExpanded, isExpanded } = useToggleExpandHelper()
@@ -59,15 +71,14 @@ export function GenericSubDevicesTable({
5971

6072
return devicesMap
6173
}, [peripheralDevices])
62-
6374
const confirmRemove = useCallback(
6475
(subdeviceId: string) => {
6576
doModalDialog({
6677
title: t('Remove this device?'),
6778
no: t('Cancel'),
6879
yes: t('Remove'),
6980
onAccept: () => {
70-
overrideHelper().deleteItem(subdeviceId).commit()
81+
instantSaveOverrideHelper().deleteItem(subdeviceId).commit()
7182
},
7283
message: (
7384
<React.Fragment>
@@ -82,7 +93,7 @@ export function GenericSubDevicesTable({
8293
),
8394
})
8495
},
85-
[t, overrideHelper]
96+
[t, instantSaveOverrideHelper]
8697
)
8798

8899
const peripheralDeviceOptions = useMemo(() => {
@@ -145,6 +156,7 @@ export function GenericSubDevicesTable({
145156
isEdited={isExpanded(item.id)}
146157
editItemWithId={toggleExpanded}
147158
removeItemWithId={confirmRemove}
159+
updatedIds={updatedIds}
148160
/>
149161
{isExpanded(item.id) && (
150162
<SubDeviceEditRow
@@ -153,6 +165,11 @@ export function GenericSubDevicesTable({
153165
editItemWithId={toggleExpanded}
154166
item={item}
155167
overrideHelper={overrideHelper}
168+
hasUnsavedChanges={hasUnsavedChanges}
169+
saveChanges={saveChanges}
170+
discardChanges={discardChanges}
171+
updateObjectId={updateObjectId}
172+
updatedIds={updatedIds}
156173
/>
157174
)}
158175
</React.Fragment>
@@ -170,13 +187,15 @@ interface SummaryRowProps {
170187
isEdited: boolean
171188
editItemWithId: (itemId: string) => void
172189
removeItemWithId: (itemId: string) => void
190+
updatedIds: Map<string, string>
173191
}
174192
function SummaryRow({
175193
item,
176194
peripheralDevice,
177195
isEdited,
178196
editItemWithId,
179197
removeItemWithId,
198+
updatedIds,
180199
}: Readonly<SummaryRowProps>): JSX.Element {
181200
const editItem = useCallback(() => editItemWithId(item.id), [editItemWithId, item.id])
182201
const removeItem = useCallback(() => removeItemWithId(item.id), [removeItemWithId, item.id])
@@ -185,13 +204,17 @@ function SummaryRow({
185204
? (peripheralDevice.subdeviceManifest?.[item.computed.options.type]?.displayName ?? '-')
186205
: '-'
187206

207+
const idChanged = Array.from(updatedIds?.entries() || []).some(([key, value]) => value === item.id || key === item.id)
208+
188209
return (
189210
<tr
190211
className={classNames({
191212
hl: isEdited,
192213
})}
193214
>
194-
<th className="settings-studio-device__name c2">{item.id}</th>
215+
<th className="settings-studio-device__name c2">
216+
{item.id} {idChanged && '(pending save)'}
217+
</th>
195218

196219
<th className="settings-studio-device__parent c2">
197220
{peripheralDevice?.name || item.computed.peripheralDeviceId || '-'}
@@ -252,31 +275,41 @@ interface SubDeviceEditRowProps {
252275
editItemWithId: (subdeviceId: string, forceState?: boolean) => void
253276
item: WrappedOverridableItemNormal<any>
254277
overrideHelper: OverrideOpHelper
278+
hasUnsavedChanges: boolean
279+
saveChanges: () => void
280+
discardChanges: () => void
281+
updateObjectId: (oldId: string, newId: string) => void
282+
updatedIds: Map<string, string>
255283
}
256284
function SubDeviceEditRow({
257285
peripheralDevice,
258286
peripheralDeviceOptions,
259287
editItemWithId,
260288
item,
261289
overrideHelper,
290+
hasUnsavedChanges,
291+
saveChanges,
292+
discardChanges,
293+
updateObjectId,
294+
updatedIds,
262295
}: Readonly<SubDeviceEditRowProps>) {
263296
const { t } = useTranslation()
264297

265298
const finishEditItem = useCallback(() => editItemWithId(item.id, false), [editItemWithId, item.id])
266299

267-
const updateObjectId = useCallback(
300+
const handleUpdateId = useCallback(
268301
(newId: string) => {
269-
if (item.id === newId) return
270-
271-
overrideHelper().changeItemId(item.id, newId).commit()
302+
updateObjectId(item.id, newId)
272303

273304
// toggle ui visibility
274305
editItemWithId(item.id, false)
275306
editItemWithId(newId, true)
276307
},
277-
[item.id, overrideHelper, editItemWithId]
308+
[item.id, updateObjectId]
278309
)
279310

311+
const idToShowInInput = updatedIds?.get(item.id) || item.id
312+
280313
return (
281314
<tr className="expando-details hl" key={item.id + '-details'}>
282315
<td colSpan={99}>
@@ -294,7 +327,7 @@ function SubDeviceEditRow({
294327
</LabelAndOverridesForDropdown>
295328
<label className="field">
296329
<LabelActual label={t('Device ID')} />
297-
<TextInputControl value={item.id} handleUpdate={updateObjectId} disabled={!!item.defaults} />
330+
<TextInputControl value={idToShowInInput} handleUpdate={handleUpdateId} disabled={!!item.defaults} />
298331
</label>
299332

300333
{!item.computed.peripheralDeviceId && (
@@ -308,9 +341,23 @@ function SubDeviceEditRow({
308341
)}
309342
</div>
310343
<div className="m-1 me-2 text-end">
311-
<button className={classNames('btn btn-primary')} onClick={finishEditItem}>
312-
<FontAwesomeIcon icon={faCheck} />
313-
</button>
344+
{hasUnsavedChanges ? (
345+
<>
346+
<button className="btn btn-warning ms-2" onClick={discardChanges}>
347+
<FontAwesomeIcon icon={faBan} />
348+
&nbsp;{t('Discard')}
349+
</button>
350+
351+
<button className="btn btn-primary ms-2" onClick={saveChanges}>
352+
<FontAwesomeIcon icon={faSave} />
353+
&nbsp;{t('Save')}
354+
</button>
355+
</>
356+
) : (
357+
<button className={classNames('btn btn-primary')} onClick={finishEditItem}>
358+
<FontAwesomeIcon icon={faCheck} />
359+
</button>
360+
)}
314361
</div>
315362
</td>
316363
</tr>

packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useMemo } from 'react'
1+
import { useCallback, useMemo, useState } from 'react'
22
import { Studios } from '../../../../collections/index.js'
33
import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids'
44
import { useTracker } from '../../../../lib/ReactMeteorData/ReactMeteorData.js'
@@ -30,6 +30,13 @@ export function StudioIngestSubDevices({
3030

3131
const studio = useTracker(() => Studios.findOne(studioId), [studioId])
3232

33+
const [unsavedOverrides, setUnsavedOverrides] = useState<SomeObjectOverrideOp[] | undefined>(undefined)
34+
35+
const baseSettings = useMemo(
36+
() => studio?.peripheralDeviceSettings?.ingestDevices ?? wrapDefaultObject({}),
37+
[studio?.peripheralDeviceSettings?.ingestDevices]
38+
)
39+
3340
const saveOverrides = useCallback(
3441
(newOps: SomeObjectOverrideOp[]) => {
3542
if (studio?._id) {
@@ -43,17 +50,25 @@ export function StudioIngestSubDevices({
4350
[studio?._id]
4451
)
4552

46-
const baseSettings = useMemo(
47-
() => studio?.peripheralDeviceSettings?.ingestDevices ?? wrapDefaultObject({}),
48-
[studio?.peripheralDeviceSettings?.ingestDevices]
49-
)
53+
const settingsWithOverrides = useMemo(() => {
54+
if (unsavedOverrides) {
55+
return {
56+
...baseSettings,
57+
overrides: unsavedOverrides,
58+
}
59+
}
60+
return baseSettings
61+
}, [baseSettings, unsavedOverrides])
5062

51-
const overrideHelper = useOverrideOpHelper(saveOverrides, baseSettings)
63+
const batchedOverrideHelper = useOverrideOpHelper(setUnsavedOverrides, settingsWithOverrides)
64+
const instantSaveOverrideHelper = useOverrideOpHelper(saveOverrides, settingsWithOverrides)
5265

5366
const wrappedSubDevices = useMemo(
5467
() =>
55-
getAllCurrentAndDeletedItemsFromOverrides<StudioIngestDevice>(baseSettings, (a, b) => a[0].localeCompare(b[0])),
56-
[baseSettings]
68+
getAllCurrentAndDeletedItemsFromOverrides<StudioIngestDevice>(settingsWithOverrides, (a, b) =>
69+
a[0].localeCompare(b[0])
70+
),
71+
[settingsWithOverrides]
5772
)
5873

5974
const filteredPeripheralDevices = useMemo(
@@ -85,7 +100,39 @@ export function StudioIngestSubDevices({
85100
'peripheralDeviceSettings.ingestDevices.overrides': addOp,
86101
},
87102
})
88-
}, [studioId, wrappedSubDevices])
103+
}, [wrappedSubDevices, settingsWithOverrides.overrides])
104+
105+
const [updatedIds, setUpdatedIds] = useState(new Map<string, string>())
106+
107+
const updateObjectId = useCallback(
108+
(oldId: string, newId: string) => {
109+
if (oldId === newId) return
110+
111+
batchedOverrideHelper().changeItemId(oldId, newId).commit()
112+
setUpdatedIds((prev) => new Map(prev).set(oldId, newId))
113+
},
114+
[batchedOverrideHelper, setUpdatedIds]
115+
)
116+
117+
const discardChanges = useCallback(() => {
118+
setUnsavedOverrides(undefined)
119+
setUpdatedIds(new Map<string, string>())
120+
}, [])
121+
122+
const saveChanges = useCallback(() => {
123+
if (studio?._id && unsavedOverrides) {
124+
Studios.update(studio._id, {
125+
$set: {
126+
'peripheralDeviceSettings.ingestDevices.overrides': unsavedOverrides,
127+
},
128+
})
129+
setUnsavedOverrides(undefined)
130+
}
131+
132+
if (updatedIds.size > 0) {
133+
setUpdatedIds(new Map<string, string>())
134+
}
135+
}, [studio?._id, unsavedOverrides])
89136

90137
return (
91138
<div className="mb-4">
@@ -101,8 +148,14 @@ export function StudioIngestSubDevices({
101148

102149
<GenericSubDevicesTable
103150
subDevices={wrappedSubDevices}
104-
overrideHelper={overrideHelper}
151+
overrideHelper={batchedOverrideHelper}
105152
peripheralDevices={filteredPeripheralDevices}
153+
hasUnsavedChanges={!!unsavedOverrides}
154+
instantSaveOverrideHelper={instantSaveOverrideHelper}
155+
saveChanges={saveChanges}
156+
discardChanges={discardChanges}
157+
updateObjectId={updateObjectId}
158+
updatedIds={updatedIds}
106159
/>
107160

108161
<div className="my-1 mx-2">

0 commit comments

Comments
 (0)