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
286 lines
8.4 KiB
TypeScript
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;
|
|
}
|
|
);
|