refactor: update document filtering logic to maintain complete dataset and clear selections on filter change

This commit is contained in:
Anish Sarkar 2026-02-06 15:47:07 +05:30
parent c8273cd413
commit e01717c65d
2 changed files with 38 additions and 31 deletions

View file

@ -140,6 +140,9 @@ export default function DocumentsTable() {
} }
}); });
setPageIndex(0); setPageIndex(0);
// Clear selections when filter changes — selected IDs from the previous
// filter view are no longer visible and would cause misleading bulk actions
setSelectedIds(new Set());
}; };
const onBulkDelete = async () => { const onBulkDelete = async () => {

View file

@ -71,8 +71,14 @@ function isValidDocument(doc: DocumentElectric): boolean {
* 3. Use syncHandle.isUpToDate to determine if deletions can be trusted * 3. Use syncHandle.isUpToDate to determine if deletions can be trusted
* 4. Handles bulk deletions correctly by checking sync state * 4. Handles bulk deletions correctly by checking sync state
* *
* Filtering strategy:
* - Internal state always stores ALL documents (unfiltered)
* - typeFilter is applied client-side when returning documents
* - typeCounts always reflect the full dataset so the filter sidebar stays complete
* - Changing filters is instant (no API re-fetch or Electric re-sync)
*
* @param searchSpaceId - The search space ID to filter documents * @param searchSpaceId - The search space ID to filter documents
* @param typeFilter - Optional document types to filter by * @param typeFilter - Optional document types to filter by (applied client-side)
*/ */
export function useDocuments( export function useDocuments(
searchSpaceId: number | null, searchSpaceId: number | null,
@ -80,7 +86,8 @@ export function useDocuments(
) { ) {
const electricClient = useElectricClient(); const electricClient = useElectricClient();
const [documents, setDocuments] = useState<DocumentDisplay[]>([]); // Internal state: ALL documents (unfiltered)
const [allDocuments, setAllDocuments] = useState<DocumentDisplay[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
@ -94,14 +101,21 @@ export function useDocuments(
const syncHandleRef = useRef<SyncHandle | null>(null); const syncHandleRef = useRef<SyncHandle | null>(null);
const liveQueryRef = useRef<{ unsubscribe?: () => void } | null>(null); const liveQueryRef = useRef<{ unsubscribe?: () => void } | null>(null);
// Real-time type counts // Type counts from ALL documents (unfiltered) — keeps filter sidebar complete
const typeCounts = useMemo(() => { const typeCounts = useMemo(() => {
const counts: Record<string, number> = {}; const counts: Record<string, number> = {};
for (const doc of documents) { for (const doc of allDocuments) {
counts[doc.document_type] = (counts[doc.document_type] || 0) + 1; counts[doc.document_type] = (counts[doc.document_type] || 0) + 1;
} }
return counts; return counts;
}, [documents]); }, [allDocuments]);
// Client-side filtered documents for display
const documents = useMemo(() => {
if (typeFilter.length === 0) return allDocuments;
const filterSet = new Set<string>(typeFilter);
return allDocuments.filter((doc) => filterSet.has(doc.document_type));
}, [allDocuments, typeFilter]);
// Populate user cache from API response // Populate user cache from API response
const populateUserCache = useCallback( const populateUserCache = useCallback(
@ -151,7 +165,8 @@ export function useDocuments(
[] []
); );
// EFFECT 1: Load from API (PRIMARY source of truth) // EFFECT 1: Load ALL documents from API (PRIMARY source of truth)
// No type filter — always fetches everything so typeCounts stay complete
useEffect(() => { useEffect(() => {
if (!searchSpaceId) { if (!searchSpaceId) {
setLoading(false); setLoading(false);
@ -160,7 +175,6 @@ export function useDocuments(
// Capture validated value for async closure // Capture validated value for async closure
const spaceId = searchSpaceId; const spaceId = searchSpaceId;
const currentTypeFilter = typeFilter;
let mounted = true; let mounted = true;
apiLoadedRef.current = false; apiLoadedRef.current = false;
@ -174,8 +188,7 @@ export function useDocuments(
queryParams: { queryParams: {
search_space_id: spaceId, search_space_id: spaceId,
page: 0, page: 0,
page_size: -1, // Fetch all documents page_size: -1, // Fetch all documents (unfiltered)
...(currentTypeFilter.length > 0 && { document_types: currentTypeFilter }),
}, },
}); });
@ -183,7 +196,7 @@ export function useDocuments(
populateUserCache(response.items); populateUserCache(response.items);
const docs = response.items.map(apiToDisplayDoc); const docs = response.items.map(apiToDisplayDoc);
setDocuments(docs); setAllDocuments(docs);
apiLoadedRef.current = true; apiLoadedRef.current = true;
setError(null); setError(null);
console.log("[useDocuments] API loaded", docs.length, "documents"); console.log("[useDocuments] API loaded", docs.length, "documents");
@ -201,16 +214,16 @@ export function useDocuments(
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [searchSpaceId, typeFilter, populateUserCache, apiToDisplayDoc]); }, [searchSpaceId, populateUserCache, apiToDisplayDoc]);
// EFFECT 2: Start Electric sync + live query for real-time updates // EFFECT 2: Start Electric sync + live query for real-time updates
// No type filter — syncs and queries ALL documents; filtering is client-side
useEffect(() => { useEffect(() => {
if (!searchSpaceId || !electricClient) return; if (!searchSpaceId || !electricClient) return;
// Capture validated values for async closure // Capture validated values for async closure
const spaceId = searchSpaceId; const spaceId = searchSpaceId;
const client = electricClient; const client = electricClient;
const currentTypeFilter = typeFilter;
let mounted = true; let mounted = true;
@ -228,7 +241,7 @@ export function useDocuments(
try { try {
console.log("[useDocuments] Starting Electric sync for real-time updates"); console.log("[useDocuments] Starting Electric sync for real-time updates");
// Start Electric sync // Start Electric sync (all documents for this search space)
const handle = await client.syncShape({ const handle = await client.syncShape({
table: "documents", table: "documents",
where: `search_space_id = ${spaceId}`, where: `search_space_id = ${spaceId}`,
@ -263,7 +276,7 @@ export function useDocuments(
if (!mounted) return; if (!mounted) return;
// Set up live query // Set up live query (unfiltered — type filtering is done client-side)
const db = client.db as { const db = client.db as {
live?: { live?: {
query: <T>( query: <T>(
@ -281,21 +294,12 @@ export function useDocuments(
return; return;
} }
let query = `SELECT id, document_type, search_space_id, title, created_by_id, created_at, status const query = `SELECT id, document_type, search_space_id, title, created_by_id, created_at, status
FROM documents FROM documents
WHERE search_space_id = $1`; WHERE search_space_id = $1
ORDER BY created_at DESC`;
const params: (number | string)[] = [spaceId]; const liveQuery = await db.live.query<DocumentElectric>(query, [spaceId]);
if (currentTypeFilter.length > 0) {
const placeholders = currentTypeFilter.map((_, i) => `$${i + 2}`).join(", ");
query += ` AND document_type IN (${placeholders})`;
params.push(...currentTypeFilter);
}
query += ` ORDER BY created_at DESC`;
const liveQuery = await db.live.query<DocumentElectric>(query, params);
if (!mounted) { if (!mounted) {
liveQuery.unsubscribe?.(); liveQuery.unsubscribe?.();
@ -333,7 +337,7 @@ export function useDocuments(
.then((response) => { .then((response) => {
populateUserCache(response.items); populateUserCache(response.items);
if (mounted) { if (mounted) {
setDocuments((prev) => setAllDocuments((prev) =>
prev.map((doc) => ({ prev.map((doc) => ({
...doc, ...doc,
created_by_name: doc.created_by_id created_by_name: doc.created_by_id
@ -347,7 +351,7 @@ export function useDocuments(
} }
// Smart update logic based on sync state // Smart update logic based on sync state
setDocuments((prev) => { setAllDocuments((prev) => {
// Don't process if API hasn't loaded yet // Don't process if API hasn't loaded yet
if (!apiLoadedRef.current) { if (!apiLoadedRef.current) {
console.log("[useDocuments] Waiting for API load, skipping live update"); console.log("[useDocuments] Waiting for API load, skipping live update");
@ -424,7 +428,7 @@ export function useDocuments(
liveQueryRef.current = null; liveQueryRef.current = null;
} }
}; };
}, [searchSpaceId, electricClient, typeFilter, electricToDisplayDoc, populateUserCache]); }, [searchSpaceId, electricClient, electricToDisplayDoc, populateUserCache]);
// Track previous searchSpaceId to detect actual changes // Track previous searchSpaceId to detect actual changes
const prevSearchSpaceIdRef = useRef<number | null>(null); const prevSearchSpaceIdRef = useRef<number | null>(null);
@ -432,7 +436,7 @@ export function useDocuments(
// Reset on search space change (not on initial mount) // Reset on search space change (not on initial mount)
useEffect(() => { useEffect(() => {
if (prevSearchSpaceIdRef.current !== null && prevSearchSpaceIdRef.current !== searchSpaceId) { if (prevSearchSpaceIdRef.current !== null && prevSearchSpaceIdRef.current !== searchSpaceId) {
setDocuments([]); setAllDocuments([]);
apiLoadedRef.current = false; apiLoadedRef.current = false;
userCacheRef.current.clear(); userCacheRef.current.clear();
} }