Skip to content

Commit 33ef155

Browse files
t3dotggclaude
andcommitted
fix(web): snooze options editor reverts after a failed save and resets mixed environments
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 258e277 commit 33ef155

2 files changed

Lines changed: 26 additions & 7 deletions

File tree

‎apps/web/src/components/settings/SnoozePresetsSettings.tsx‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ import {
3030
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
3131
import { SettingResetButton, SettingsRow } from "./settingsLayout";
3232
import { searchableSetting } from "./settingsSearch";
33-
import { useScopedSettings, useUpdateScopedSettings } from "./useScopedSettings";
33+
import {
34+
useScopedSettings,
35+
useScopedSettingsMixed,
36+
useUpdateScopedSettings,
37+
} from "./useScopedSettings";
3438

3539
const UNIT_MINUTES = { minutes: 1, hours: 60, days: 24 * 60 } as const;
3640
type DurationUnit = keyof typeof UNIT_MINUTES;
@@ -116,7 +120,10 @@ export function SnoozePresetsRow() {
116120
// Row keys survive removals, so a row's in-progress field state stays with that row.
117121
const [keys, setKeys] = useState<ReadonlyArray<number>>(() => saved.map((_, index) => index));
118122
const [nextKey, setNextKey] = useState(saved.length);
119-
const [savedSource, setSavedSource] = useState(saved);
123+
// Null forces the next render to adopt the saved value, as after a failed write.
124+
const [savedSource, setSavedSource] = useState<ReadonlyArray<SnoozePresetSetting> | null>(saved);
125+
// Environments disagree, so equal-looking edits and Reset must still write to all of them.
126+
const mixed = useScopedSettingsMixed(["snoozePresets"]);
120127
const [written, setWritten] = useState<ReadonlyArray<ReadonlyArray<SnoozePresetSetting>>>([]);
121128
const freshKeys = (count: number) => {
122129
setNextKey(nextKey + count);
@@ -137,11 +144,19 @@ export function SnoozePresetsRow() {
137144
snoozePresets: ReadonlyArray<SnoozePresetSetting>,
138145
nextKeys: ReadonlyArray<number> = keys,
139146
) => {
140-
if (Equal.equals(snoozePresets, presets)) return;
147+
if (!mixed && Equal.equals(snoozePresets, presets)) return;
141148
setPresets(snoozePresets);
142149
setKeys(nextKeys);
143150
setWritten((previous) => [...previous, snoozePresets]);
144-
updateSettings({ snoozePresets });
151+
updateSettings(
152+
{ snoozePresets },
153+
{
154+
onFailed: () => {
155+
setWritten([]);
156+
setSavedSource(null);
157+
},
158+
},
159+
);
145160
};
146161
const replaceAt = (index: number, next: SnoozePresetSetting) =>
147162
save(presets.map((preset, candidate) => (candidate === index ? next : preset)));
@@ -153,7 +168,7 @@ export function SnoozePresetsRow() {
153168
{...searchableSetting("snooze-options")}
154169
description="Choices in every snooze menu, listed soonest first. Weekdays mean the next one after today."
155170
resetAction={
156-
Equal.equals(presets, DEFAULT_SNOOZE_PRESETS) ? null : (
171+
!mixed && Equal.equals(presets, DEFAULT_SNOOZE_PRESETS) ? null : (
157172
<SettingResetButton
158173
label="snooze options"
159174
onClick={() => save(DEFAULT_SNOOZE_PRESETS, freshKeys(DEFAULT_SNOOZE_PRESETS.length))}

‎apps/web/src/components/settings/useScopedSettings.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,14 @@ export function useScopedSettingSource(keys: readonly (keyof ServerSettings)[])
4949

5050
function useRunScopedPlan() {
5151
const persistServer = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
52-
return (plan: ReturnType<typeof planScopedSettingsPatch>) => {
52+
return (plan: ReturnType<typeof planScopedSettingsPatch>, onFailed?: () => void) => {
5353
if (plan.unavailableReason) {
5454
toastManager.add({
5555
type: "warning",
5656
title: "Setting not saved",
5757
description: plan.unavailableReason,
5858
});
59+
onFailed?.();
5960
return;
6061
}
6162
void persistScopedSettingsPatch(
@@ -78,6 +79,7 @@ function useRunScopedPlan() {
7879
persistClientSettingsPatch,
7980
).then(({ failedEnvironments, savedEnvironments, savedEnvironmentCount }) => {
8081
if (failedEnvironments.length === 0) return;
82+
onFailed?.();
8183
toastManager.add({
8284
type: "error",
8385
title:
@@ -98,7 +100,9 @@ function useRunScopedPlan() {
98100
export function useUpdateScopedSettings() {
99101
const { scope, environments } = useSettingsScope();
100102
const run = useRunScopedPlan();
101-
return (patch: ScopedSettingsPatch) => run(planScopedSettingsPatch(scope, environments, patch));
103+
/** `onFailed` runs when any target rejects or drops the patch, so a draft can fall back. */
104+
return (patch: ScopedSettingsPatch, options?: { readonly onFailed?: () => void }) =>
105+
run(planScopedSettingsPatch(scope, environments, patch), options?.onFailed);
102106
}
103107

104108
export function useClearScopedSettings() {

0 commit comments

Comments
 (0)