88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
// Copyright (c) 2026 Patrick Motsch
|
|
// All rights reserved.
|
|
/**
|
|
* useTreeExpansion - fire-and-forget persistence for tree expand state.
|
|
*
|
|
* Simple contract:
|
|
* - On mount: load saved expandedIds from backend (or null if none).
|
|
* - Returns the loaded ids (once) so the tree can seed its initial state.
|
|
* - Provides a `save(ids)` function that debounce-PUTs to the backend.
|
|
* - No bidirectional state flow, no props, no re-render triggers.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import api from '../api';
|
|
|
|
const _SAVE_DEBOUNCE_MS = 600;
|
|
|
|
export interface UseTreeExpansionResult {
|
|
loaded: boolean;
|
|
initialIds: string[] | null;
|
|
save: (ids: string[]) => void;
|
|
}
|
|
|
|
export function useTreeExpansion(
|
|
instanceId: string | null | undefined,
|
|
scope: string,
|
|
): UseTreeExpansionResult {
|
|
const [loaded, setLoaded] = useState(false);
|
|
const [initialIds, setInitialIds] = useState<string[] | null>(null);
|
|
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const latestRef = useRef<string[] | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!instanceId) {
|
|
setLoaded(true);
|
|
setInitialIds(null);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
setLoaded(false);
|
|
api
|
|
.get(`/api/workspace/${instanceId}/ui-tree-expansion/${encodeURIComponent(scope)}`)
|
|
.then((res) => {
|
|
if (cancelled) return;
|
|
const fromServer: string[] | null = res.data?.expandedNodes ?? null;
|
|
setInitialIds(fromServer);
|
|
latestRef.current = fromServer;
|
|
setLoaded(true);
|
|
})
|
|
.catch((err) => {
|
|
if (cancelled) return;
|
|
console.warn('[useTreeExpansion] load failed', err);
|
|
setInitialIds(null);
|
|
setLoaded(true);
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [instanceId, scope]);
|
|
|
|
const save = useCallback(
|
|
(ids: string[]) => {
|
|
if (!instanceId) return;
|
|
const sorted = [...ids].sort().join('|');
|
|
const prevSorted = latestRef.current ? [...latestRef.current].sort().join('|') : null;
|
|
if (sorted === prevSorted) return;
|
|
latestRef.current = ids;
|
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
saveTimerRef.current = setTimeout(() => {
|
|
api
|
|
.put(
|
|
`/api/workspace/${instanceId}/ui-tree-expansion/${encodeURIComponent(scope)}`,
|
|
{ expandedNodes: latestRef.current ?? [] },
|
|
)
|
|
.catch((err) => {
|
|
console.warn('[useTreeExpansion] save failed', err);
|
|
});
|
|
}, _SAVE_DEBOUNCE_MS);
|
|
},
|
|
[instanceId, scope],
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
};
|
|
}, []);
|
|
|
|
return { loaded, initialIds, save };
|
|
}
|