Skip to content

Commit 5a071d4

Browse files
trangdoan982claude
andauthored
ENG-514: Auto-save plugin settings (#913)
* ENG-514: Auto-save plugin settings and remove Save Changes buttons Settings now save automatically matching Obsidian's native settings UX: - Discrete inputs (toggles, dropdowns, color pickers): save on change - Text inputs with validation (node type fields, relation type labels): save on blur - Validation errors shown inline on the affected row instead of Notice toasts - Removed Save Changes button and unsaved changes indicator from all tabs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * address PR comments --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 51a394b commit 5a071d4

5 files changed

Lines changed: 259 additions & 282 deletions

File tree

apps/obsidian/src/components/AdminPanelSettings.tsx

Lines changed: 19 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,27 @@ export const AdminPanelSettings = () => {
88
const [syncModeEnabled, setSyncModeEnabled] = useState<boolean>(
99
plugin.settings.syncModeEnabled ?? false,
1010
);
11-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
1211

13-
const handleSyncModeToggle = useCallback((newValue: boolean) => {
14-
setSyncModeEnabled(newValue);
15-
setHasUnsavedChanges(true);
16-
}, []);
12+
const handleSyncModeToggle = useCallback(
13+
async (newValue: boolean) => {
14+
setSyncModeEnabled(newValue);
15+
plugin.settings.syncModeEnabled = newValue;
16+
await plugin.saveSettings();
1717

18-
const handleSave = async () => {
19-
plugin.settings.syncModeEnabled = syncModeEnabled;
20-
await plugin.saveSettings();
21-
new Notice("Admin panel settings saved");
22-
setHasUnsavedChanges(false);
23-
24-
if (syncModeEnabled) {
25-
try {
26-
await initializeSupabaseSync(plugin);
27-
new Notice("Sync mode initialized successfully");
28-
} catch (error) {
29-
console.error("Failed to initialize sync mode:", error);
30-
new Notice(
31-
`Failed to initialize sync mode: ${error instanceof Error ? error.message : String(error)}`,
32-
);
18+
if (newValue) {
19+
try {
20+
await initializeSupabaseSync(plugin);
21+
new Notice("Sync mode initialized successfully");
22+
} catch (error) {
23+
console.error("Failed to initialize sync mode:", error);
24+
new Notice(
25+
`Failed to initialize sync mode: ${error instanceof Error ? error.message : String(error)}`,
26+
);
27+
}
3328
}
34-
}
35-
};
29+
},
30+
[plugin],
31+
);
3632

3733
return (
3834
<div className="general-settings">
@@ -46,26 +42,12 @@ export const AdminPanelSettings = () => {
4642
<div className="setting-item-control">
4743
<div
4844
className={`checkbox-container ${syncModeEnabled ? "is-enabled" : ""}`}
49-
onClick={() => handleSyncModeToggle(!syncModeEnabled)}
45+
onClick={() => void handleSyncModeToggle(!syncModeEnabled)}
5046
>
5147
<input type="checkbox" checked={syncModeEnabled} />
5248
</div>
5349
</div>
5450
</div>
55-
56-
<div className="setting-item">
57-
<button
58-
onClick={() => void handleSave()}
59-
className={hasUnsavedChanges ? "mod-cta" : ""}
60-
disabled={!hasUnsavedChanges}
61-
>
62-
Save Changes
63-
</button>
64-
</div>
65-
66-
{hasUnsavedChanges && (
67-
<div className="text-muted mt-2">You have unsaved changes</div>
68-
)}
6951
</div>
7052
);
7153
};

apps/obsidian/src/components/GeneralSettings.tsx

Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useCallback, useRef, useEffect } from "react";
22
import { usePlugin } from "./PluginContext";
3-
import { Notice, setIcon } from "obsidian";
3+
import { setIcon } from "obsidian";
44
import SuggestInput from "./SuggestInput";
55
import { SLACK_LOGO, WHITE_LOGO_SVG } from "~/icons";
66

@@ -157,57 +157,51 @@ const GeneralSettings = () => {
157157
const [nodeTagHotkey, setNodeTagHotkey] = useState<string>(
158158
plugin.settings.nodeTagHotkey,
159159
);
160-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
161160

162161
const handleToggleChange = (newValue: boolean) => {
163162
setShowIdsInFrontmatter(newValue);
164-
setHasUnsavedChanges(true);
163+
plugin.settings.showIdsInFrontmatter = newValue;
164+
void plugin.saveSettings();
165165
};
166166

167-
const handleFolderPathChange = useCallback((newValue: string) => {
168-
setNodesFolderPath(newValue);
169-
setHasUnsavedChanges(true);
170-
}, []);
167+
const handleFolderPathChange = useCallback(
168+
(newValue: string) => {
169+
setNodesFolderPath(newValue);
170+
plugin.settings.nodesFolderPath = newValue.trim();
171+
void plugin.saveSettings();
172+
},
173+
[plugin],
174+
);
171175

172-
const handleCanvasFolderPathChange = useCallback((newValue: string) => {
173-
setCanvasFolderPath(newValue);
174-
setHasUnsavedChanges(true);
175-
}, []);
176+
const handleCanvasFolderPathChange = useCallback(
177+
(newValue: string) => {
178+
setCanvasFolderPath(newValue);
179+
plugin.settings.canvasFolderPath = newValue.trim();
180+
void plugin.saveSettings();
181+
},
182+
[plugin],
183+
);
176184

177185
const handleCanvasAttachmentsFolderPathChange = useCallback(
178186
(newValue: string) => {
179187
setCanvasAttachmentsFolderPath(newValue);
180-
setHasUnsavedChanges(true);
188+
plugin.settings.canvasAttachmentsFolderPath = newValue.trim();
189+
void plugin.saveSettings();
181190
},
182-
[],
191+
[plugin],
183192
);
184193

185-
const handleNodeTagHotkeyChange = useCallback((newValue: string) => {
186-
// Only allow single character
187-
if (newValue.length <= 1) {
188-
setNodeTagHotkey(newValue);
189-
setHasUnsavedChanges(true);
190-
}
191-
}, []);
192-
193-
const handleSave = async () => {
194-
const trimmedNodesFolderPath = nodesFolderPath.trim();
195-
const trimmedCanvasFolderPath = canvasFolderPath.trim();
196-
const trimmedCanvasAttachmentsFolderPath =
197-
canvasAttachmentsFolderPath.trim();
198-
plugin.settings.showIdsInFrontmatter = showIdsInFrontmatter;
199-
plugin.settings.nodesFolderPath = trimmedNodesFolderPath;
200-
plugin.settings.canvasFolderPath = trimmedCanvasFolderPath;
201-
plugin.settings.canvasAttachmentsFolderPath =
202-
trimmedCanvasAttachmentsFolderPath;
203-
plugin.settings.nodeTagHotkey = nodeTagHotkey || "";
204-
setNodesFolderPath(trimmedNodesFolderPath);
205-
setCanvasFolderPath(trimmedCanvasFolderPath);
206-
setCanvasAttachmentsFolderPath(trimmedCanvasAttachmentsFolderPath);
207-
await plugin.saveSettings();
208-
new Notice("General settings saved");
209-
setHasUnsavedChanges(false);
210-
};
194+
const handleNodeTagHotkeyChange = useCallback(
195+
(newValue: string) => {
196+
// Only allow single character
197+
if (newValue.length <= 1) {
198+
setNodeTagHotkey(newValue);
199+
plugin.settings.nodeTagHotkey = newValue;
200+
void plugin.saveSettings();
201+
}
202+
},
203+
[plugin],
204+
);
211205

212206
return (
213207
<div className="general-settings">
@@ -313,19 +307,6 @@ const GeneralSettings = () => {
313307
</div>
314308
</div>
315309

316-
<div className="setting-item">
317-
<button
318-
onClick={() => void handleSave()}
319-
className={hasUnsavedChanges ? "mod-cta" : ""}
320-
disabled={!hasUnsavedChanges}
321-
>
322-
Save Changes
323-
</button>
324-
</div>
325-
326-
{hasUnsavedChanges && (
327-
<div className="text-muted mt-2">You have unsaved changes</div>
328-
)}
329310
<InfoSection />
330311
</div>
331312
);

0 commit comments

Comments
 (0)