Files
oranguru/src/Griddy/adapters/ResolveSpecAdapter.tsx
Hein 7244bd33fc refactor(advancedSearch): reorder exports and improve type definitions
refactor(types): reorganize SearchCondition and AdvancedSearchState interfaces
refactor(filterPresets): streamline useFilterPresets hook and localStorage handling
refactor(filtering): clean up ColumnFilterButton and ColumnFilterPopover components
refactor(loading): separate GriddyLoadingOverlay from GriddyLoadingSkeleton
refactor(searchHistory): enhance useSearchHistory hook with persistence
refactor(index): update exports for adapters and core components
refactor(rendering): improve EditableCell and TableCell components for clarity
refactor(rendering): enhance TableHeader and VirtualBody components for better readability
2026-02-15 19:54:33 +02:00

286 lines
8.4 KiB
TypeScript

import { getResolveSpecClient } from '@warkypublic/resolvespec-js';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import type { AdapterConfig, AdapterRef } from './types';
import { useGriddyStore } from '../core/GriddyStore';
import { applyCursor, buildOptions } from './mapOptions';
export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
function ResolveSpecAdapter(props, ref) {
const {
autoFetch = true,
baseUrl,
columnMap,
computedColumns,
cursorField = 'id',
customOperators,
debounceMs = 300,
defaultOptions,
entity,
mode = 'cursor',
pageSize = 25,
preload,
schema,
token,
} = props;
const sorting = useGriddyStore((s) => s.sorting ?? []);
const columnFilters = useGriddyStore((s) => s.columnFilters ?? []);
const pagination = useGriddyStore((s) => s.pagination);
const setData = useGriddyStore((s) => s.setData);
const appendData = useGriddyStore((s) => s.appendData);
const setDataCount = useGriddyStore((s) => s.setDataCount);
const setIsLoading = useGriddyStore((s) => s.setIsLoading);
const setError = useGriddyStore((s) => s.setError);
const setInfiniteScroll = useGriddyStore((s) => s.setInfiniteScroll);
const clientRef = useRef(getResolveSpecClient({ baseUrl, token }));
const debounceRef = useRef<null | ReturnType<typeof setTimeout>>(null);
const mountedRef = useRef(true);
// Cursor state (only used in cursor mode)
const cursorRef = useRef<null | string>(null);
const hasMoreRef = useRef(true);
const [cursorLoading, setCursorLoading] = useState(false);
// Update client if baseUrl/token changes
useEffect(() => {
clientRef.current = getResolveSpecClient({ baseUrl, token });
}, [baseUrl, token]);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
// ─── Offset mode fetch (original behavior) ───
const fetchDataOffset = useCallback(async () => {
if (!mountedRef.current) return;
setIsLoading(true);
setError(null);
try {
const paginationState = pagination?.enabled
? { pageIndex: 0, pageSize: pagination.pageSize }
: undefined;
const options = buildOptions(
sorting,
columnFilters,
paginationState,
columnMap,
defaultOptions
);
if (preload) options.preload = preload;
if (computedColumns) options.computedColumns = computedColumns;
if (customOperators) options.customOperators = customOperators;
const response = await clientRef.current.read(schema, entity, undefined, options);
if (!mountedRef.current) return;
if (response.success) {
setData(Array.isArray(response.data) ? response.data : [response.data]);
if (response.metadata?.total != null) {
setDataCount(response.metadata.total);
}
} else if (response.error) {
setError(new Error(response.error.message ?? 'Request failed'));
}
} catch (err) {
if (mountedRef.current) {
setError(err instanceof Error ? err : new Error(String(err)));
}
} finally {
if (mountedRef.current) {
setIsLoading(false);
}
}
}, [
sorting,
columnFilters,
pagination,
columnMap,
defaultOptions,
preload,
computedColumns,
customOperators,
schema,
entity,
setData,
setDataCount,
setIsLoading,
setError,
]);
// ─── Cursor mode fetch ───
const fetchCursorPage = useCallback(
async (cursor: null | string, isAppend: boolean) => {
if (!mountedRef.current) return;
if (isAppend) {
setCursorLoading(true);
} else {
setIsLoading(true);
}
setError(null);
try {
const options = buildOptions(
sorting,
columnFilters,
undefined,
columnMap,
defaultOptions
);
if (preload) options.preload = preload;
if (computedColumns) options.computedColumns = computedColumns;
if (customOperators) options.customOperators = customOperators;
const cursorOptions = applyCursor(options, cursor, pageSize);
const response = await clientRef.current.read(schema, entity, undefined, cursorOptions);
if (!mountedRef.current) return;
if (response.success) {
const rows = Array.isArray(response.data) ? response.data : [response.data];
if (isAppend) {
appendData(rows);
} else {
setData(rows);
}
if (response.metadata?.total != null) {
setDataCount(response.metadata.total);
}
// Extract cursor from last row
if (rows.length > 0) {
const lastRow = rows[rows.length - 1];
cursorRef.current =
lastRow?.[cursorField] != null ? String(lastRow[cursorField]) : null;
}
// Determine hasMore
hasMoreRef.current = rows.length >= pageSize;
} else if (response.error) {
setError(new Error(response.error.message ?? 'Request failed'));
}
} catch (err) {
if (mountedRef.current) {
setError(err instanceof Error ? err : new Error(String(err)));
}
} finally {
if (mountedRef.current) {
if (isAppend) {
setCursorLoading(false);
} else {
setIsLoading(false);
}
}
}
},
[
sorting,
columnFilters,
columnMap,
defaultOptions,
preload,
computedColumns,
customOperators,
schema,
entity,
pageSize,
cursorField,
setData,
appendData,
setDataCount,
setIsLoading,
setError,
]
);
const fetchNextPage = useCallback(() => {
if (!hasMoreRef.current || cursorLoading) return;
fetchCursorPage(cursorRef.current, true);
}, [cursorLoading, fetchCursorPage]);
const resetAndFetch = useCallback(async () => {
cursorRef.current = null;
hasMoreRef.current = true;
await fetchCursorPage(null, false);
}, [fetchCursorPage]);
// ─── Unified fetch dispatch ───
const fetchData = mode === 'cursor' ? resetAndFetch : fetchDataOffset;
// ─── Infinite scroll config sync (cursor mode only) ───
useEffect(() => {
// Skip infinite scroll if not in cursor mode OR if pagination is explicitly enabled
if (mode !== 'cursor' || pagination?.enabled) {
setInfiniteScroll(undefined);
return;
}
setInfiniteScroll({
enabled: true,
hasMore: hasMoreRef.current,
isLoading: cursorLoading,
onLoadMore: fetchNextPage,
threshold: 10,
});
}, [mode, pagination?.enabled, cursorLoading, fetchNextPage, setInfiniteScroll]);
// Cleanup infinite scroll on unmount
useEffect(() => {
return () => {
setInfiniteScroll(undefined);
};
}, [setInfiniteScroll]);
useImperativeHandle(ref, () => ({ refetch: fetchData }), [fetchData]);
// Auto-fetch on mount
const initialFetchDone = useRef(false);
useEffect(() => {
if (autoFetch && !initialFetchDone.current) {
initialFetchDone.current = true;
fetchData();
}
}, [autoFetch, fetchData]);
// Debounced re-fetch on state changes (skip initial)
const prevDepsRef = useRef<null | string>(null);
useEffect(() => {
const depsKey =
mode === 'cursor'
? JSON.stringify({ columnFilters, sorting })
: JSON.stringify({ columnFilters, pagination, sorting });
if (prevDepsRef.current === null) {
prevDepsRef.current = depsKey;
return;
}
if (prevDepsRef.current === depsKey) return;
prevDepsRef.current = depsKey;
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(fetchData, debounceMs);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [sorting, columnFilters, pagination, debounceMs, fetchData, mode]);
return null;
}
);