From d9846ea02b211373b1ba45cd84837bec2537c815 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 17 Jun 2026 15:41:26 -0600 Subject: [PATCH 1/7] firmware renders with warning styling if it wasn't found in the server --- src/device/VersionChip.tsx | 8 ++++++++ src/pbHelpers/FirmwareVersion.tsx | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/device/VersionChip.tsx b/src/device/VersionChip.tsx index 09dac0f..8826239 100644 --- a/src/device/VersionChip.tsx +++ b/src/device/VersionChip.tsx @@ -22,6 +22,14 @@ class VersionChip extends React.Component { variant="outlined" label={firmwareVersionHelper.description} icon={firmwareVersionHelper.icon} + sx={ + firmwareVersionHelper.colour + ? { + borderColor: firmwareVersionHelper.colour, + color: firmwareVersionHelper.colour + } + : undefined + } /> ); diff --git a/src/pbHelpers/FirmwareVersion.tsx b/src/pbHelpers/FirmwareVersion.tsx index d7dcc8c..43ea776 100644 --- a/src/pbHelpers/FirmwareVersion.tsx +++ b/src/pbHelpers/FirmwareVersion.tsx @@ -9,6 +9,7 @@ export interface FirmwareVersionHelper { const oldFirmwareColour = "var(--status-warning)"; const currentFirmwareColour = "var(--status-ok)"; +const unknownFirmwareColour = "var(--status-warning)"; export function getFirmwareVersionHelper( version: string, @@ -17,24 +18,34 @@ export function getFirmwareVersionHelper( const firmwareIcon = ; const oldFirmwareIcon = ; const currentFirmwareIcon = ; + const unknownFirmwareIcon = ; let helper = {} as FirmwareVersionHelper; if (!version || version === "") { helper.description = "Unknown version"; helper.icon = oldFirmwareIcon; helper.tooltip = "We don't know what version your device is, it may behave unexpectedly"; + helper.colour = oldFirmwareColour; + } else if (version.startsWith("!")) { + helper.description = version; + helper.icon = unknownFirmwareIcon; + helper.tooltip = "This firmware version was reported by the device but is missing from the firmware list"; + helper.colour = unknownFirmwareColour; } else if (available !== "" && version !== available) { helper.description = version; helper.icon = oldFirmwareIcon; helper.tooltip = "A firmware upgrade is available, some features may be disabled for out-of-date devices"; + helper.colour = oldFirmwareColour; } else if (available === "") { helper.description = version; helper.icon = firmwareIcon; helper.tooltip = "Running version " + version; + helper.colour = ""; } else { helper.description = version; helper.icon = currentFirmwareIcon; helper.tooltip = "Your device is running the latest firmware"; + helper.colour = currentFirmwareColour; } return helper; } From 0984ae0f37efed9f48b21e4f60f14dd957b09a9b Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 17 Jun 2026 16:19:35 -0600 Subject: [PATCH 2/7] got rid of warning signifier on firmware print --- src/pbHelpers/FirmwareVersion.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pbHelpers/FirmwareVersion.tsx b/src/pbHelpers/FirmwareVersion.tsx index 43ea776..49472be 100644 --- a/src/pbHelpers/FirmwareVersion.tsx +++ b/src/pbHelpers/FirmwareVersion.tsx @@ -26,9 +26,10 @@ export function getFirmwareVersionHelper( helper.tooltip = "We don't know what version your device is, it may behave unexpectedly"; helper.colour = oldFirmwareColour; } else if (version.startsWith("!")) { - helper.description = version; + const cleanVersion = version.substring(1); + helper.description = cleanVersion; helper.icon = unknownFirmwareIcon; - helper.tooltip = "This firmware version was reported by the device but is missing from the firmware list"; + helper.tooltip = "Firmware " + cleanVersion + " was reported by the device but is missing from the firmware list"; helper.colour = unknownFirmwareColour; } else if (available !== "" && version !== available) { helper.description = version; From 895ed8e50ba269cf47cba35d6510dacdb6807626 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 24 Jun 2026 13:13:14 -0600 Subject: [PATCH 3/7] opening interaction websocket to detect accepted pending changes --- src/interactions/InteractionsOverview.tsx | 45 ++++++++++++++++++++--- src/pages/Device.tsx | 43 +++++++++++++++++++++- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index 600c88a..db73675 100644 --- a/src/interactions/InteractionsOverview.tsx +++ b/src/interactions/InteractionsOverview.tsx @@ -4,6 +4,7 @@ import { CardActions, CardContent, CardHeader, + CircularProgress, darken, Grid2 as Grid, IconButton, @@ -14,7 +15,7 @@ import { Tooltip, Typography } from "@mui/material"; -import { Settings } from "@mui/icons-material"; +import { CheckCircleOutline, Settings } from "@mui/icons-material"; import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks"; import { cloneDeep } from "lodash"; import { Component, Device, Interaction } from "models"; @@ -80,6 +81,7 @@ export default function InteractionsOverview(props: Props) { const [interactions, setInteractions] = useState(props.interactions); const [dirtyInteractions, setDirtyInteractions] = useState>(new Map()); const [mappedComponents, setMappedComponents] = useState>(new Map()); + const [acceptedInteractions, setAcceptedInteractions] = useState>(new Set()); const [renderToggle, setRenderToggle] = useState(false); const [selectedInteraction, setSelectedInteraction] = useState( undefined @@ -95,6 +97,20 @@ export default function InteractionsOverview(props: Props) { } if (props.interactions !== prevInteractions) { + if (prevInteractions) { + setAcceptedInteractions((prevAccepted) => { + const updatedAccepted = new Set(prevAccepted); + props.interactions.forEach((interaction: Interaction) => { + const previous = prevInteractions.find(prevInteraction => prevInteraction.key() === interaction.key()); + if (!interaction.status.synced) { + updatedAccepted.delete(interaction.key()); + } else if (previous && !previous.status.synced) { + updatedAccepted.add(interaction.key()); + } + }); + return updatedAccepted; + }); + } setInteractions(cloneDeep(props.interactions)); } }, [components, prevComponents, props.interactions, prevInteractions]); @@ -226,10 +242,16 @@ export default function InteractionsOverview(props: Props) { let key = interaction.key(); let source = mappedComponents.get(componentIDToString(interaction.settings.source)); let sink = mappedComponents.get(componentIDToString(interaction.settings.sink)); + const isPending = !interaction.status.synced && Boolean(interaction.status.lastUpdate); + const isAccepted = interaction.status.synced && acceptedInteractions.has(interaction.key()); let statusText = - !interaction.status.synced && interaction.status.lastUpdate - ? "Pending " + - moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow() + !interaction.status.synced + ? interaction.status.lastUpdate + ? "Pending " + + moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow() + : "" + : acceptedInteractions.has(interaction.key()) + ? "Pending change accepted" : ""; let schedule = pond.InteractionSchedule.create( interaction.settings.schedule !== null ? interaction.settings.schedule : undefined @@ -272,7 +294,20 @@ export default function InteractionsOverview(props: Props) { className={classes.header} titleTypographyProps={{ variant: "subtitle2" }} title={interactionResultText(interaction, sink)} - subheader={statusText} + subheader={ + statusText ? ( + + {isPending && } + {isAccepted && ( + + )} + + {statusText} + + + ) : ""} subheaderTypographyProps={{ variant: "caption" }} action={action} /> diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 6c9535d..96b06b6 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -282,6 +282,45 @@ export default function DevicePage() { enabled: !!deviceID, }); + // --- Real-time interaction updates --- + // Streams interaction changes for this device, including status changes + // when the device accepts pending interaction updates. + useWebSocket<{ key: string; interaction: Interaction } | null>({ + path: `/v1/live/devices/${deviceID}/interactions`, + parse: (e) => { + try { + const raw = JSON.parse(e.data); + const interaction = Interaction.any(raw); + if (!interaction?.settings) { + console.warn("[ws] received interaction without settings:", raw); + return null; + } + return { key: interaction.key(), interaction }; + } catch (err) { + console.warn("[ws] failed to parse interaction:", err); + return null; + } + }, + onMessage: (data) => { + if (!data) return; + const { key, interaction } = data; + setInteractions((prev) => { + const index = prev.findIndex((existing) => existing.key() === key); + if (index < 0) { + return [...prev, interaction]; + } + const updated = [...prev]; + updated[index] = interaction; + return updated; + }); + }, + keys: liveContextKeys, + types: liveContextTypes, + as: liveAs, + token, + enabled: !!deviceID, + }); + const loadPortScan = () => { deviceAPI.listFoundComponents(parseInt(deviceID)) .then(resp => { @@ -395,7 +434,7 @@ export default function DevicePage() { interactions={filteredInteractions} permissions={permissions} deviceComponentPreferences={prefsMap.get(c.key())} - key={i} + key={c.key()} refreshCallback={(updatedComponent?: Component) => updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice() } @@ -412,7 +451,7 @@ export default function DevicePage() { interactions={filteredInteractions} permissions={permissions} deviceComponentPreferences={prefsMap.get(c.key())} - key={i} + key={c.key()} refreshCallback={(updatedComponent?: Component) => updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice() } From 879a5ae1f08bc9b4109c50dabdfe189a22e9daab Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 24 Jun 2026 14:16:54 -0600 Subject: [PATCH 4/7] fixed pending state logic --- src/interactions/InteractionsOverview.tsx | 60 ++++++++++++++--------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index db73675..2619e97 100644 --- a/src/interactions/InteractionsOverview.tsx +++ b/src/interactions/InteractionsOverview.tsx @@ -70,6 +70,11 @@ interface Props { refreshCallback: () => void; } +interface PendingInteraction { + since: string; + accepted: boolean; +} + export default function InteractionsOverview(props: Props) { const classes = useStyles(); const [{as, user}] = useGlobalState(); @@ -81,7 +86,7 @@ export default function InteractionsOverview(props: Props) { const [interactions, setInteractions] = useState(props.interactions); const [dirtyInteractions, setDirtyInteractions] = useState>(new Map()); const [mappedComponents, setMappedComponents] = useState>(new Map()); - const [acceptedInteractions, setAcceptedInteractions] = useState>(new Set()); + const [pendingInteractions, setPendingInteractions] = useState>(new Map()); const [renderToggle, setRenderToggle] = useState(false); const [selectedInteraction, setSelectedInteraction] = useState( undefined @@ -97,20 +102,23 @@ export default function InteractionsOverview(props: Props) { } if (props.interactions !== prevInteractions) { - if (prevInteractions) { - setAcceptedInteractions((prevAccepted) => { - const updatedAccepted = new Set(prevAccepted); - props.interactions.forEach((interaction: Interaction) => { - const previous = prevInteractions.find(prevInteraction => prevInteraction.key() === interaction.key()); - if (!interaction.status.synced) { - updatedAccepted.delete(interaction.key()); - } else if (previous && !previous.status.synced) { - updatedAccepted.add(interaction.key()); + setPendingInteractions((prevPending) => { + const updatedPending = new Map(prevPending); + props.interactions.forEach((interaction: Interaction) => { + const existing = updatedPending.get(interaction.key()); + if (!interaction.status.synced) { + if (!existing || existing.accepted) { + updatedPending.set(interaction.key(), { + since: interaction.status.lastUpdate || moment().toISOString(), + accepted: false + }); } - }); - return updatedAccepted; + } else if (existing && !existing.accepted) { + updatedPending.set(interaction.key(), { ...existing, accepted: true }); + } }); - } + return updatedPending; + }); setInteractions(cloneDeep(props.interactions)); } }, [components, prevComponents, props.interactions, prevInteractions]); @@ -217,6 +225,14 @@ export default function InteractionsOverview(props: Props) { index: number, settings: pond.IInteractionSettings ) => { + const interactionKey = settings.key ?? ""; + setPendingInteractions((prevPending) => { + const updatedPending = new Map(prevPending); + if (interactionKey) { + updatedPending.set(interactionKey, { since: moment().toISOString(), accepted: false }); + } + return updatedPending; + }); interactionsAPI .updateInteraction(Number(deviceID), settings, as) .then((_response: any) => { @@ -224,7 +240,6 @@ export default function InteractionsOverview(props: Props) { updatedDirtyInteractions.set(index, false); setDirtyInteractions(updatedDirtyInteractions); success("Successfully updated the interaction for " + component.name()); - refreshCallback(); }) .catch((_err: any) => { error("Error occurred while updating the interaction for " + component.name()); @@ -242,16 +257,13 @@ export default function InteractionsOverview(props: Props) { let key = interaction.key(); let source = mappedComponents.get(componentIDToString(interaction.settings.source)); let sink = mappedComponents.get(componentIDToString(interaction.settings.sink)); - const isPending = !interaction.status.synced && Boolean(interaction.status.lastUpdate); - const isAccepted = interaction.status.synced && acceptedInteractions.has(interaction.key()); - let statusText = - !interaction.status.synced - ? interaction.status.lastUpdate - ? "Pending " + - moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow() - : "" - : acceptedInteractions.has(interaction.key()) - ? "Pending change accepted" + const pending = pendingInteractions.get(interaction.key()); + const isAccepted = Boolean(pending?.accepted); + const isPending = Boolean(pending && !pending.accepted); + let statusText = isAccepted + ? "Pending change accepted" + : isPending + ? "Pending " + moment(pending.since).fromNow() : ""; let schedule = pond.InteractionSchedule.create( interaction.settings.schedule !== null ? interaction.settings.schedule : undefined From b9a13c578115c23fe2558a7b04b15e472bf98329 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 24 Jun 2026 14:40:04 -0600 Subject: [PATCH 5/7] fixed pending change race condition --- src/interactions/InteractionsOverview.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index 2619e97..b579660 100644 --- a/src/interactions/InteractionsOverview.tsx +++ b/src/interactions/InteractionsOverview.tsx @@ -72,6 +72,7 @@ interface Props { interface PendingInteraction { since: string; + seenUnsynced: boolean; accepted: boolean; } @@ -110,10 +111,13 @@ export default function InteractionsOverview(props: Props) { if (!existing || existing.accepted) { updatedPending.set(interaction.key(), { since: interaction.status.lastUpdate || moment().toISOString(), + seenUnsynced: true, accepted: false }); + } else if (!existing.seenUnsynced) { + updatedPending.set(interaction.key(), { ...existing, seenUnsynced: true }); } - } else if (existing && !existing.accepted) { + } else if (existing && existing.seenUnsynced && !existing.accepted) { updatedPending.set(interaction.key(), { ...existing, accepted: true }); } }); @@ -229,7 +233,7 @@ export default function InteractionsOverview(props: Props) { setPendingInteractions((prevPending) => { const updatedPending = new Map(prevPending); if (interactionKey) { - updatedPending.set(interactionKey, { since: moment().toISOString(), accepted: false }); + updatedPending.set(interactionKey, { since: moment().toISOString(), seenUnsynced: false, accepted: false }); } return updatedPending; }); From e8cc5099150384a2838f517af98b72d93adffdbd Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 25 Jun 2026 12:15:49 -0600 Subject: [PATCH 6/7] less restrictive on receiving websockets --- src/interactions/InteractionsOverview.tsx | 85 ++++++++++++++--------- src/pages/Device.tsx | 19 +++++ 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index b579660..450c44f 100644 --- a/src/interactions/InteractionsOverview.tsx +++ b/src/interactions/InteractionsOverview.tsx @@ -70,12 +70,6 @@ interface Props { refreshCallback: () => void; } -interface PendingInteraction { - since: string; - seenUnsynced: boolean; - accepted: boolean; -} - export default function InteractionsOverview(props: Props) { const classes = useStyles(); const [{as, user}] = useGlobalState(); @@ -87,12 +81,21 @@ export default function InteractionsOverview(props: Props) { const [interactions, setInteractions] = useState(props.interactions); const [dirtyInteractions, setDirtyInteractions] = useState>(new Map()); const [mappedComponents, setMappedComponents] = useState>(new Map()); - const [pendingInteractions, setPendingInteractions] = useState>(new Map()); + const [acceptedInteractions, setAcceptedInteractions] = useState>(new Set()); const [renderToggle, setRenderToggle] = useState(false); const [selectedInteraction, setSelectedInteraction] = useState( undefined ); + useEffect(() => { + console.log("[interactions:mount] initial interactions:", props.interactions.map(i => ({ + key: i.key(), + synced: i.status.synced, + lastUpdate: i.status.lastUpdate, + lastSynced: i.status.lastSynced, + }))); + }, []); + useEffect(() => { if (components !== prevComponents) { let initMappedComponents: Map = new Map(); @@ -103,25 +106,30 @@ export default function InteractionsOverview(props: Props) { } if (props.interactions !== prevInteractions) { - setPendingInteractions((prevPending) => { - const updatedPending = new Map(prevPending); + console.log("[interactions:effect] props.interactions changed, prevInteractions was", prevInteractions ? "defined" : "undefined"); + props.interactions.forEach((interaction: Interaction) => { + const key = interaction.key(); + const prevInteraction = prevInteractions?.find(p => p.key() === key); + console.log("[interactions:effect]", key, { + synced: interaction.status.synced, + lastUpdate: interaction.status.lastUpdate, + prevSynced: prevInteraction?.status.synced, + prevLastUpdate: prevInteraction?.status.lastUpdate, + }); + }); + setAcceptedInteractions(prev => { + const updated = new Set(prev); props.interactions.forEach((interaction: Interaction) => { - const existing = updatedPending.get(interaction.key()); + const key = interaction.key(); + const prevInteraction = prevInteractions?.find(p => p.key() === key); if (!interaction.status.synced) { - if (!existing || existing.accepted) { - updatedPending.set(interaction.key(), { - since: interaction.status.lastUpdate || moment().toISOString(), - seenUnsynced: true, - accepted: false - }); - } else if (!existing.seenUnsynced) { - updatedPending.set(interaction.key(), { ...existing, seenUnsynced: true }); - } - } else if (existing && existing.seenUnsynced && !existing.accepted) { - updatedPending.set(interaction.key(), { ...existing, accepted: true }); + updated.delete(key); + } else if (prevInteraction && !prevInteraction.status.synced) { + console.log("[interactions:effect] ACCEPTED", key); + updated.add(key); } }); - return updatedPending; + return updated; }); setInteractions(cloneDeep(props.interactions)); } @@ -229,20 +237,30 @@ export default function InteractionsOverview(props: Props) { index: number, settings: pond.IInteractionSettings ) => { - const interactionKey = settings.key ?? ""; - setPendingInteractions((prevPending) => { - const updatedPending = new Map(prevPending); - if (interactionKey) { - updatedPending.set(interactionKey, { since: moment().toISOString(), seenUnsynced: false, accepted: false }); - } - return updatedPending; - }); interactionsAPI .updateInteraction(Number(deviceID), settings, as) .then((_response: any) => { + console.log("[interactions:submit] API success, setting synced=false for index", index); let updatedDirtyInteractions = cloneDeep(dirtyInteractions); updatedDirtyInteractions.set(index, false); setDirtyInteractions(updatedDirtyInteractions); + setInteractions(prev => { + const updated = cloneDeep(prev); + if (updated[index]) { + updated[index].status.synced = false; + updated[index].status.lastUpdate = moment().toISOString(); + console.log("[interactions:submit] local state updated for", updated[index].key()); + } + return updated; + }); + const interactionKey = settings.key ?? ""; + if (interactionKey) { + setAcceptedInteractions(prev => { + const updated = new Set(prev); + updated.delete(interactionKey); + return updated; + }); + } success("Successfully updated the interaction for " + component.name()); }) .catch((_err: any) => { @@ -261,13 +279,12 @@ export default function InteractionsOverview(props: Props) { let key = interaction.key(); let source = mappedComponents.get(componentIDToString(interaction.settings.source)); let sink = mappedComponents.get(componentIDToString(interaction.settings.sink)); - const pending = pendingInteractions.get(interaction.key()); - const isAccepted = Boolean(pending?.accepted); - const isPending = Boolean(pending && !pending.accepted); + const isAccepted = acceptedInteractions.has(interaction.key()); + const isPending = !interaction.status.synced; let statusText = isAccepted ? "Pending change accepted" : isPending - ? "Pending " + moment(pending.since).fromNow() + ? "Pending " + moment(interaction.status.lastUpdate).fromNow() : ""; let schedule = pond.InteractionSchedule.create( interaction.settings.schedule !== null ? interaction.settings.schedule : undefined diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 96b06b6..733d621 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -304,11 +304,30 @@ export default function DevicePage() { onMessage: (data) => { if (!data) return; const { key, interaction } = data; + console.log("[ws:interaction] received", key, { + synced: interaction.status.synced, + lastUpdate: interaction.status.lastUpdate, + lastSynced: interaction.status.lastSynced, + }); setInteractions((prev) => { const index = prev.findIndex((existing) => existing.key() === key); if (index < 0) { return [...prev, interaction]; } + const existing = prev[index]; + const existingLastUpdate = getTimestampMillis(existing.status.lastUpdate); + const incomingLastUpdate = getTimestampMillis(interaction.status.lastUpdate); + const incomingLastSynced = getTimestampMillis(interaction.status.lastSynced); + if (existingLastUpdate !== undefined && incomingLastUpdate !== undefined && + existingLastUpdate > incomingLastUpdate) { + return prev; + } + if (!existing.status.synced && interaction.status.synced && + incomingLastUpdate !== undefined && + existingLastUpdate === incomingLastUpdate && + (incomingLastSynced === undefined || incomingLastSynced < incomingLastUpdate)) { + return prev; + } const updated = [...prev]; updated[index] = interaction; return updated; From ffd5e0a76902bc8d4813a31740377289fc0f5587 Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 25 Jun 2026 12:33:14 -0600 Subject: [PATCH 7/7] preventing form updates when settings dialog is open --- src/component/ComponentSettings.tsx | 3 ++- src/device/DeviceSettings.tsx | 6 ++++-- src/interactions/InteractionSettings.tsx | 9 +++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/component/ComponentSettings.tsx b/src/component/ComponentSettings.tsx index db0e5ad..c703393 100644 --- a/src/component/ComponentSettings.tsx +++ b/src/component/ComponentSettings.tsx @@ -213,7 +213,8 @@ export default function ComponentSettings(props: Props) { }, [props.component, componentTypeOptions]); useEffect(() => { - if (props.component !== prevComponent || (!isDialogOpen && prevIsDialogOpen)) { + if (isDialogOpen && prevIsDialogOpen) return; + if (props.component !== prevComponent || isDialogOpen !== prevIsDialogOpen) { init(); } }, [props.component, prevComponent, init, isDialogOpen, prevIsDialogOpen]); diff --git a/src/device/DeviceSettings.tsx b/src/device/DeviceSettings.tsx index a9277f5..26ca591 100644 --- a/src/device/DeviceSettings.tsx +++ b/src/device/DeviceSettings.tsx @@ -95,6 +95,7 @@ export default function DeviceSettings(props: Props) { const deviceAPI = useDeviceAPI(); const { device, isDialogOpen, closeDialogCallback, canEdit, refreshCallback, components } = props; const prevDevice = usePrevious(device); + const prevIsDialogOpen = usePrevious(isDialogOpen); const [deviceForm, setDeviceForm] = useState(deviceFromForm(Device.clone(device))); const [isRemoveDeviceDialogOpen, setIsRemoveDeviceDialogOpen] = useState(false); const [sleeps, setSleeps] = useState(device.settings.sleepDurationS > 0); @@ -110,7 +111,8 @@ export default function DeviceSettings(props: Props) { const [existingMutation, setExistingMutation] = useState(); useEffect(() => { - if (prevDevice !== device) { + if (isDialogOpen && prevIsDialogOpen) return; + if (prevDevice !== device || isDialogOpen !== prevIsDialogOpen) { setDeviceForm(deviceFromForm(Device.clone(props.device))); if (device.settings.extensionComponents[0]) { setCompExtOne(device.settings.extensionComponents[0]); @@ -130,7 +132,7 @@ export default function DeviceSettings(props: Props) { setComponentsByDevice(cbd); } } - }, [device, prevDevice, props.device, components]); + }, [device, prevDevice, props.device, components, isDialogOpen, prevIsDialogOpen]); const close = () => { closeDialogCallback(); diff --git a/src/interactions/InteractionSettings.tsx b/src/interactions/InteractionSettings.tsx index 4f2a2d3..c98742f 100644 --- a/src/interactions/InteractionSettings.tsx +++ b/src/interactions/InteractionSettings.tsx @@ -133,6 +133,7 @@ export default function InteractionSettings(props: Props) { const prevInitialInteraction = usePrevious(initialInteraction); const prevComponents = usePrevious(components); const prevInitialComponent = usePrevious(initialComponent); + const prevIsDialogOpen = usePrevious(isDialogOpen); const classes = useStyles(); const [{ user, as }] = useGlobalState(); const interactionsAPI = useInteractionsAPI(); @@ -232,10 +233,12 @@ export default function InteractionSettings(props: Props) { if (user && user.settings.timezone) { setTimezone(user.settings.timezone); } + if (isDialogOpen && prevIsDialogOpen) return; if ( prevInitialInteraction !== initialInteraction || (prevComponents && prevComponents.length !== components.length) || - getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent) + getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent) || + isDialogOpen !== prevIsDialogOpen ) { setDefaultState(); } @@ -247,7 +250,9 @@ export default function InteractionSettings(props: Props) { prevInitialComponent, prevInitialInteraction, setDefaultState, - user + user, + isDialogOpen, + prevIsDialogOpen ]); const getAvailableSinks = () => {