ui-nyla/src/hooks/useTreeExpansion.ts
2026-05-19 16:47:52 +02:00

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 };
}