fix: Cell Identity changes now show Configuration changes pending banner
Unit Tests / test (push) Successful in 7m26s

DraftConfig dirty state (set when any Cell Identity field changes) was
tracked in refs but never checked by the banner, which only looked at
backend pending state. Cell name changes in pic_ngo mode intentionally
block auto-save (to prevent premature DDNS re-registration), so the
backend never marked pending and the banner never appeared.

Fix: show the banner when hasDirty() is true in addition to backend
pending. Add clearAllDirty() to DraftConfigContext so Cancel immediately
clears frontend dirty state without waiting for the next 5-second poll.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 16:17:51 -04:00
parent 2085f77733
commit ec8995d41e
2 changed files with 9 additions and 4 deletions
+4 -3
View File
@@ -188,7 +188,7 @@ function AppCore() {
const [applyStatus, setApplyStatus] = useState(null); // null | 'saving' | 'restarting' | 'done' | 'timeout' | 'error' const [applyStatus, setApplyStatus] = useState(null); // null | 'saving' | 'restarting' | 'done' | 'timeout' | 'error'
const [applyError, setApplyError] = useState(''); const [applyError, setApplyError] = useState('');
const { flushAll, hasDirty } = useDraftConfig(); const { flushAll, hasDirty, clearAllDirty } = useDraftConfig();
const handleApply = useCallback(async () => { const handleApply = useCallback(async () => {
setApplyError(''); setApplyError('');
@@ -231,10 +231,11 @@ function AppCore() {
}, [flushAll, hasDirty]); }, [flushAll, hasDirty]);
const handleCancel = useCallback(async () => { const handleCancel = useCallback(async () => {
clearAllDirty();
await cellAPI.cancelPending(); await cellAPI.cancelPending();
setPending({ needs_restart: false, changes: [] }); setPending({ needs_restart: false, changes: [] });
window.dispatchEvent(new CustomEvent('pic-config-discarded')); window.dispatchEvent(new CustomEvent('pic-config-discarded'));
}, []); }, [clearAllDirty]);
const [activeServiceChildren, setActiveServiceChildren] = useState([]); const [activeServiceChildren, setActiveServiceChildren] = useState([]);
@@ -327,7 +328,7 @@ function AppCore() {
</div> </div>
)} )}
{isOnline && pending.needs_restart && !applyStatus && ( {isOnline && (pending.needs_restart || hasDirty()) && !applyStatus && (
<PendingRestartBanner pending={pending} onApply={handleApply} onCancel={handleCancel} /> <PendingRestartBanner pending={pending} onApply={handleApply} onCancel={handleCancel} />
)} )}
+5 -1
View File
@@ -25,8 +25,12 @@ export function DraftConfigProvider({ children }) {
await Promise.all(flushers.map(fn => fn())); await Promise.all(flushers.map(fn => fn()));
}, []); }, []);
const clearAllDirty = useCallback(() => {
hasDirtyRef.current = {};
}, []);
return ( return (
<DraftConfigContext.Provider value={{ registerFlusher, setDirty, hasDirty, flushAll }}> <DraftConfigContext.Provider value={{ registerFlusher, setDirty, hasDirty, flushAll, clearAllDirty }}>
{children} {children}
</DraftConfigContext.Provider> </DraftConfigContext.Provider>
); );