mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-30 21:59:46 +02:00
refactor: integrate useDocumentSearch hook into DocumentsSidebar for improved search functionality and performance, while enhancing user feedback with localized toast messages
This commit is contained in:
parent
62c4a3befc
commit
be0bfb3d56
8 changed files with 162 additions and 121 deletions
|
|
@ -9,8 +9,9 @@ import { toast } from "sonner";
|
||||||
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||||
import { useDocuments, toDisplayDoc, type DocumentDisplay } from "@/hooks/use-documents";
|
import { useDocuments } from "@/hooks/use-documents";
|
||||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
import { useDocumentSearch } from "@/hooks/use-document-search";
|
||||||
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import { DocumentsFilters } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters";
|
import { DocumentsFilters } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters";
|
||||||
import {
|
import {
|
||||||
|
|
@ -19,18 +20,6 @@ import {
|
||||||
} from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell";
|
} from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell";
|
||||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||||
|
|
||||||
const SEARCH_INITIAL_SIZE = 20;
|
|
||||||
const SEARCH_SCROLL_SIZE = 5;
|
|
||||||
|
|
||||||
function useDebounced<T>(value: T, delay = 250) {
|
|
||||||
const [debounced, setDebounced] = useState(value);
|
|
||||||
useEffect(() => {
|
|
||||||
const t = setTimeout(() => setDebounced(value), delay);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
}, [value, delay]);
|
|
||||||
return debounced;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocumentsSidebarProps {
|
interface DocumentsSidebarProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
|
@ -44,14 +33,15 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
|
||||||
const searchSpaceId = Number(params.search_space_id);
|
const searchSpaceId = Number(params.search_space_id);
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const debouncedSearch = useDebounced(search, 250);
|
const debouncedSearch = useDebouncedValue(search, 250);
|
||||||
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
||||||
const [sortKey, setSortKey] = useState<SortKey>("created_at");
|
const [sortKey, setSortKey] = useState<SortKey>("created_at");
|
||||||
const [sortDesc, setSortDesc] = useState(true);
|
const [sortDesc, setSortDesc] = useState(true);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||||
const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom);
|
const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom);
|
||||||
|
|
||||||
// Paginated realtime documents from the hook (server-side sorted)
|
const isSearchMode = !!debouncedSearch.trim();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
documents: realtimeDocuments,
|
documents: realtimeDocuments,
|
||||||
typeCounts: realtimeTypeCounts,
|
typeCounts: realtimeTypeCounts,
|
||||||
|
|
@ -62,88 +52,22 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
|
||||||
error: realtimeError,
|
error: realtimeError,
|
||||||
} = useDocuments(searchSpaceId, activeTypes, sortKey, sortDesc ? "desc" : "asc");
|
} = useDocuments(searchSpaceId, activeTypes, sortKey, sortDesc ? "desc" : "asc");
|
||||||
|
|
||||||
const isSearchMode = !!debouncedSearch.trim();
|
const {
|
||||||
|
documents: searchDocuments,
|
||||||
|
loading: searchLoading,
|
||||||
|
loadingMore: searchLoadingMore,
|
||||||
|
hasMore: searchHasMore,
|
||||||
|
loadMore: searchLoadMore,
|
||||||
|
error: searchError,
|
||||||
|
removeItems: searchRemoveItems,
|
||||||
|
} = useDocumentSearch(searchSpaceId, debouncedSearch, activeTypes, isSearchMode && open);
|
||||||
|
|
||||||
// --- Search mode state ---
|
const displayDocs = isSearchMode ? searchDocuments : realtimeDocuments;
|
||||||
const searchApiLoadedRef = useRef(0);
|
const loading = isSearchMode ? searchLoading : realtimeLoading;
|
||||||
const [searchItems, setSearchItems] = useState<DocumentDisplay[]>([]);
|
const error = isSearchMode ? searchError : !!realtimeError;
|
||||||
const [searchLoadingMore, setSearchLoadingMore] = useState(false);
|
|
||||||
const [searchInitialLoading, setSearchInitialLoading] = useState(false);
|
|
||||||
const [searchHasMore, setSearchHasMore] = useState(false);
|
|
||||||
const searchQueryRef = useRef(debouncedSearch);
|
|
||||||
|
|
||||||
// Initial search fetch when search query changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSearchMode || !searchSpaceId || !open) {
|
|
||||||
setSearchItems([]);
|
|
||||||
setSearchHasMore(false);
|
|
||||||
searchApiLoadedRef.current = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchQueryRef.current = debouncedSearch;
|
|
||||||
setSearchInitialLoading(true);
|
|
||||||
|
|
||||||
documentsApiService
|
|
||||||
.searchDocuments({
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
page: 0,
|
|
||||||
page_size: SEARCH_INITIAL_SIZE,
|
|
||||||
title: debouncedSearch.trim(),
|
|
||||||
...(activeTypes.length > 0 && { document_types: activeTypes }),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (searchQueryRef.current !== debouncedSearch) return;
|
|
||||||
const mapped = response.items.map(toDisplayDoc);
|
|
||||||
setSearchItems(mapped);
|
|
||||||
setSearchHasMore(response.has_more);
|
|
||||||
searchApiLoadedRef.current = response.items.length;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("[DocumentsSidebar] Search failed:", err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setSearchInitialLoading(false);
|
|
||||||
});
|
|
||||||
}, [debouncedSearch, searchSpaceId, open, isSearchMode, activeTypes]);
|
|
||||||
|
|
||||||
// Load more search results (uses skip for correct offset with mixed page sizes)
|
|
||||||
const loadMoreSearch = useCallback(async () => {
|
|
||||||
if (searchLoadingMore || !isSearchMode || !searchHasMore) return;
|
|
||||||
|
|
||||||
setSearchLoadingMore(true);
|
|
||||||
try {
|
|
||||||
const response = await documentsApiService.searchDocuments({
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
skip: searchApiLoadedRef.current,
|
|
||||||
page_size: SEARCH_SCROLL_SIZE,
|
|
||||||
title: debouncedSearch.trim(),
|
|
||||||
...(activeTypes.length > 0 && { document_types: activeTypes }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (searchQueryRef.current !== debouncedSearch) return;
|
|
||||||
|
|
||||||
const mapped = response.items.map(toDisplayDoc);
|
|
||||||
setSearchItems((prev) => [...prev, ...mapped]);
|
|
||||||
setSearchHasMore(response.has_more);
|
|
||||||
searchApiLoadedRef.current += response.items.length;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[DocumentsSidebar] Load more search failed:", err);
|
|
||||||
} finally {
|
|
||||||
setSearchLoadingMore(false);
|
|
||||||
}
|
|
||||||
}, [searchLoadingMore, isSearchMode, searchHasMore, searchSpaceId, debouncedSearch, activeTypes]);
|
|
||||||
|
|
||||||
// Unified interface — pick between realtime and search mode
|
|
||||||
const displayDocs = isSearchMode ? searchItems : realtimeDocuments;
|
|
||||||
const loading = isSearchMode ? searchInitialLoading : realtimeLoading;
|
|
||||||
const error = isSearchMode ? false : realtimeError;
|
|
||||||
const hasMore = isSearchMode ? searchHasMore : realtimeHasMore;
|
const hasMore = isSearchMode ? searchHasMore : realtimeHasMore;
|
||||||
const loadingMore = isSearchMode ? searchLoadingMore : realtimeLoadingMore;
|
const loadingMore = isSearchMode ? searchLoadingMore : realtimeLoadingMore;
|
||||||
const onLoadMore = isSearchMode ? loadMoreSearch : realtimeLoadMore;
|
const onLoadMore = isSearchMode ? searchLoadMore : realtimeLoadMore;
|
||||||
|
|
||||||
const onToggleType = (type: DocumentTypeEnum, checked: boolean) => {
|
const onToggleType = (type: DocumentTypeEnum, checked: boolean) => {
|
||||||
setActiveTypes((prev) => {
|
setActiveTypes((prev) => {
|
||||||
|
|
@ -161,20 +85,14 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allDocs = isSearchMode
|
const selectedDocs = displayDocs.filter((doc) => selectedIds.has(doc.id));
|
||||||
? searchItems.map((item) => ({ id: item.id, status: item.status }))
|
|
||||||
: realtimeDocuments.map((doc) => ({ id: doc.id, status: doc.status }));
|
|
||||||
|
|
||||||
const selectedDocs = allDocs.filter((doc) => selectedIds.has(doc.id));
|
|
||||||
const deletableIds = selectedDocs
|
const deletableIds = selectedDocs
|
||||||
.filter((doc) => doc.status?.state !== "pending" && doc.status?.state !== "processing")
|
.filter((doc) => doc.status?.state !== "pending" && doc.status?.state !== "processing")
|
||||||
.map((doc) => doc.id);
|
.map((doc) => doc.id);
|
||||||
const inProgressCount = selectedIds.size - deletableIds.length;
|
const inProgressCount = selectedIds.size - deletableIds.length;
|
||||||
|
|
||||||
if (inProgressCount > 0) {
|
if (inProgressCount > 0) {
|
||||||
toast.warning(
|
toast.warning(t("delete_in_progress_warning", { count: inProgressCount }));
|
||||||
`${inProgressCount} document(s) are pending or processing and cannot be deleted.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deletableIds.length === 0) return;
|
if (deletableIds.length === 0) return;
|
||||||
|
|
@ -199,12 +117,12 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
|
||||||
if (okCount === deletableIds.length) {
|
if (okCount === deletableIds.length) {
|
||||||
toast.success(t("delete_success_count", { count: okCount }));
|
toast.success(t("delete_success_count", { count: okCount }));
|
||||||
} else if (conflictCount > 0) {
|
} else if (conflictCount > 0) {
|
||||||
toast.error(`${conflictCount} document(s) started processing. Please try again later.`);
|
toast.error(t("delete_conflict_error", { count: conflictCount }));
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("delete_partial_failed"));
|
toast.error(t("delete_partial_failed"));
|
||||||
}
|
}
|
||||||
if (isSearchMode) {
|
if (isSearchMode) {
|
||||||
setSearchItems((prev) => prev.filter((item) => !deletableIds.includes(item.id)));
|
searchRemoveItems(deletableIds);
|
||||||
}
|
}
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -219,7 +137,7 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
|
||||||
await deleteDocumentMutation({ id });
|
await deleteDocumentMutation({ id });
|
||||||
toast.success(t("delete_success") || "Document deleted");
|
toast.success(t("delete_success") || "Document deleted");
|
||||||
if (isSearchMode) {
|
if (isSearchMode) {
|
||||||
setSearchItems((prev) => prev.filter((item) => item.id !== id));
|
searchRemoveItems([id]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -227,7 +145,7 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[deleteDocumentMutation, isSearchMode, t]
|
[deleteDocumentMutation, isSearchMode, t, searchRemoveItems]
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortKeyRef = useRef(sortKey);
|
const sortKeyRef = useRef(sortKey);
|
||||||
|
|
|
||||||
127
surfsense_web/hooks/use-document-search.ts
Normal file
127
surfsense_web/hooks/use-document-search.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||||
|
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||||
|
import { type DocumentDisplay, toDisplayDoc } from "./use-documents";
|
||||||
|
|
||||||
|
const SEARCH_INITIAL_SIZE = 20;
|
||||||
|
const SEARCH_SCROLL_SIZE = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated document search hook.
|
||||||
|
*
|
||||||
|
* Handles title-based search with server-side filtering,
|
||||||
|
* pagination via skip/page_size, and staleness detection
|
||||||
|
* so fast typing never renders stale results.
|
||||||
|
*
|
||||||
|
* @param searchSpaceId - The search space to search within
|
||||||
|
* @param query - The debounced search query
|
||||||
|
* @param activeTypes - Document types to filter by
|
||||||
|
* @param enabled - When false the hook resets and stops fetching
|
||||||
|
*/
|
||||||
|
export function useDocumentSearch(
|
||||||
|
searchSpaceId: number,
|
||||||
|
query: string,
|
||||||
|
activeTypes: DocumentTypeEnum[],
|
||||||
|
enabled: boolean
|
||||||
|
) {
|
||||||
|
const [documents, setDocuments] = useState<DocumentDisplay[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [hasMore, setHasMore] = useState(false);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
const apiLoadedRef = useRef(0);
|
||||||
|
const queryRef = useRef(query);
|
||||||
|
|
||||||
|
const isActive = enabled && !!query.trim();
|
||||||
|
const activeTypesKey = activeTypes.join(",");
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTypesKey serializes activeTypes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive || !searchSpaceId) {
|
||||||
|
setDocuments([]);
|
||||||
|
setHasMore(false);
|
||||||
|
setError(false);
|
||||||
|
apiLoadedRef.current = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
queryRef.current = query;
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
|
||||||
|
documentsApiService
|
||||||
|
.searchDocuments({
|
||||||
|
queryParams: {
|
||||||
|
search_space_id: searchSpaceId,
|
||||||
|
page: 0,
|
||||||
|
page_size: SEARCH_INITIAL_SIZE,
|
||||||
|
title: query.trim(),
|
||||||
|
...(activeTypes.length > 0 && { document_types: activeTypes }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (cancelled || queryRef.current !== query) return;
|
||||||
|
setDocuments(response.items.map(toDisplayDoc));
|
||||||
|
setHasMore(response.has_more);
|
||||||
|
apiLoadedRef.current = response.items.length;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
console.error("[useDocumentSearch] Search failed:", err);
|
||||||
|
setError(true);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [query, searchSpaceId, isActive, activeTypesKey]);
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTypesKey serializes activeTypes
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (loadingMore || !isActive || !hasMore) return;
|
||||||
|
|
||||||
|
setLoadingMore(true);
|
||||||
|
try {
|
||||||
|
const response = await documentsApiService.searchDocuments({
|
||||||
|
queryParams: {
|
||||||
|
search_space_id: searchSpaceId,
|
||||||
|
skip: apiLoadedRef.current,
|
||||||
|
page_size: SEARCH_SCROLL_SIZE,
|
||||||
|
title: query.trim(),
|
||||||
|
...(activeTypes.length > 0 && { document_types: activeTypes }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (queryRef.current !== query) return;
|
||||||
|
|
||||||
|
setDocuments((prev) => [...prev, ...response.items.map(toDisplayDoc)]);
|
||||||
|
setHasMore(response.has_more);
|
||||||
|
apiLoadedRef.current += response.items.length;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[useDocumentSearch] Load more failed:", err);
|
||||||
|
} finally {
|
||||||
|
setLoadingMore(false);
|
||||||
|
}
|
||||||
|
}, [loadingMore, isActive, hasMore, searchSpaceId, query, activeTypesKey]);
|
||||||
|
|
||||||
|
const removeItems = useCallback((ids: number[]) => {
|
||||||
|
const idSet = new Set(ids);
|
||||||
|
setDocuments((prev) => prev.filter((item) => !idSet.has(item.id)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
documents,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
hasMore,
|
||||||
|
loadMore,
|
||||||
|
error,
|
||||||
|
removeItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -105,7 +105,6 @@ export function useDocuments(
|
||||||
// Snapshot of all doc IDs from Electric's first callback after initial load.
|
// Snapshot of all doc IDs from Electric's first callback after initial load.
|
||||||
// Anything appearing in subsequent callbacks NOT in this set is genuinely new.
|
// Anything appearing in subsequent callbacks NOT in this set is genuinely new.
|
||||||
const electricBaselineIdsRef = useRef<Set<number> | null>(null);
|
const electricBaselineIdsRef = useRef<Set<number> | null>(null);
|
||||||
const knownApiIdsRef = useRef<Set<number>>(new Set());
|
|
||||||
const userCacheRef = useRef<Map<string, string>>(new Map());
|
const userCacheRef = useRef<Map<string, string>>(new Map());
|
||||||
const emailCacheRef = useRef<Map<string, string>>(new Map());
|
const emailCacheRef = useRef<Map<string, string>>(new Map());
|
||||||
const syncHandleRef = useRef<SyncHandle | null>(null);
|
const syncHandleRef = useRef<SyncHandle | null>(null);
|
||||||
|
|
@ -178,7 +177,6 @@ export function useDocuments(
|
||||||
apiLoadedCountRef.current = 0;
|
apiLoadedCountRef.current = 0;
|
||||||
initialLoadDoneRef.current = false;
|
initialLoadDoneRef.current = false;
|
||||||
electricBaselineIdsRef.current = null;
|
electricBaselineIdsRef.current = null;
|
||||||
knownApiIdsRef.current = new Set();
|
|
||||||
|
|
||||||
const fetchInitialData = async () => {
|
const fetchInitialData = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -209,9 +207,6 @@ export function useDocuments(
|
||||||
setError(null);
|
setError(null);
|
||||||
apiLoadedCountRef.current = docsResponse.items.length;
|
apiLoadedCountRef.current = docsResponse.items.length;
|
||||||
initialLoadDoneRef.current = true;
|
initialLoadDoneRef.current = true;
|
||||||
for (const doc of docs) {
|
|
||||||
knownApiIdsRef.current.add(doc.id);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
console.error("[useDocuments] Initial load failed:", err);
|
console.error("[useDocuments] Initial load failed:", err);
|
||||||
|
|
@ -454,7 +449,6 @@ export function useDocuments(
|
||||||
apiLoadedCountRef.current = 0;
|
apiLoadedCountRef.current = 0;
|
||||||
initialLoadDoneRef.current = false;
|
initialLoadDoneRef.current = false;
|
||||||
electricBaselineIdsRef.current = null;
|
electricBaselineIdsRef.current = null;
|
||||||
knownApiIdsRef.current = new Set();
|
|
||||||
userCacheRef.current.clear();
|
userCacheRef.current.clear();
|
||||||
emailCacheRef.current.clear();
|
emailCacheRef.current.clear();
|
||||||
}
|
}
|
||||||
|
|
@ -481,9 +475,6 @@ export function useDocuments(
|
||||||
|
|
||||||
populateUserCache(response.items);
|
populateUserCache(response.items);
|
||||||
const newDocs = response.items.map(apiToDisplayDoc);
|
const newDocs = response.items.map(apiToDisplayDoc);
|
||||||
for (const doc of newDocs) {
|
|
||||||
knownApiIdsRef.current.add(doc.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDocuments((prev) => {
|
setDocuments((prev) => {
|
||||||
const existingIds = new Set(prev.map((d) => d.id));
|
const existingIds = new Set(prev.map((d) => d.id));
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,8 @@
|
||||||
"upload_documents": "Upload Documents",
|
"upload_documents": "Upload Documents",
|
||||||
"create_shared_note": "Create Shared Note",
|
"create_shared_note": "Create Shared Note",
|
||||||
"processing_documents": "Processing documents...",
|
"processing_documents": "Processing documents...",
|
||||||
"active_tasks_count": "{count} active task(s)"
|
"delete_in_progress_warning": "{count} document(s) are pending or processing and cannot be deleted.",
|
||||||
|
"delete_conflict_error": "{count} document(s) started processing. Please try again later."
|
||||||
},
|
},
|
||||||
"add_connector": {
|
"add_connector": {
|
||||||
"title": "Connect Your Tools",
|
"title": "Connect Your Tools",
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,8 @@
|
||||||
"upload_documents": "Subir documentos",
|
"upload_documents": "Subir documentos",
|
||||||
"create_shared_note": "Crear nota compartida",
|
"create_shared_note": "Crear nota compartida",
|
||||||
"processing_documents": "Procesando documentos...",
|
"processing_documents": "Procesando documentos...",
|
||||||
"active_tasks_count": "{count} tarea(s) activa(s)"
|
"delete_in_progress_warning": "{count} documento(s) están pendientes o en proceso y no se pueden eliminar.",
|
||||||
|
"delete_conflict_error": "{count} documento(s) comenzaron a procesarse. Inténtelo de nuevo más tarde."
|
||||||
},
|
},
|
||||||
"add_connector": {
|
"add_connector": {
|
||||||
"title": "Conecta tus herramientas",
|
"title": "Conecta tus herramientas",
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,8 @@
|
||||||
"upload_documents": "दस्तावेज़ अपलोड करें",
|
"upload_documents": "दस्तावेज़ अपलोड करें",
|
||||||
"create_shared_note": "साझा नोट बनाएं",
|
"create_shared_note": "साझा नोट बनाएं",
|
||||||
"processing_documents": "दस्तावेज़ प्रोसेस हो रहे हैं...",
|
"processing_documents": "दस्तावेज़ प्रोसेस हो रहे हैं...",
|
||||||
"active_tasks_count": "{count} सक्रिय कार्य"
|
"delete_in_progress_warning": "{count} दस्तावेज़ लंबित या प्रसंस्करण में हैं और हटाए नहीं जा सकते।",
|
||||||
|
"delete_conflict_error": "{count} दस्तावेज़ प्रसंस्करण शुरू हो गया है। कृपया बाद में पुनः प्रयास करें।"
|
||||||
},
|
},
|
||||||
"add_connector": {
|
"add_connector": {
|
||||||
"title": "अपने टूल कनेक्ट करें",
|
"title": "अपने टूल कनेक्ट करें",
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,8 @@
|
||||||
"upload_documents": "Enviar documentos",
|
"upload_documents": "Enviar documentos",
|
||||||
"create_shared_note": "Criar nota compartilhada",
|
"create_shared_note": "Criar nota compartilhada",
|
||||||
"processing_documents": "Processando documentos...",
|
"processing_documents": "Processando documentos...",
|
||||||
"active_tasks_count": "{count} tarefa(s) ativa(s)"
|
"delete_in_progress_warning": "{count} documento(s) estão pendentes ou em processamento e não podem ser excluídos.",
|
||||||
|
"delete_conflict_error": "{count} documento(s) começaram a ser processados. Tente novamente mais tarde."
|
||||||
},
|
},
|
||||||
"add_connector": {
|
"add_connector": {
|
||||||
"title": "Conecte suas ferramentas",
|
"title": "Conecte suas ferramentas",
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,8 @@
|
||||||
"upload_documents": "上传文档",
|
"upload_documents": "上传文档",
|
||||||
"create_shared_note": "创建共享笔记",
|
"create_shared_note": "创建共享笔记",
|
||||||
"processing_documents": "正在处理文档...",
|
"processing_documents": "正在处理文档...",
|
||||||
"active_tasks_count": "{count} 个正在进行的工作项"
|
"delete_in_progress_warning": "{count} 个文档正在等待或处理中,无法删除。",
|
||||||
|
"delete_conflict_error": "{count} 个文档已开始处理,请稍后再试。"
|
||||||
},
|
},
|
||||||
"add_connector": {
|
"add_connector": {
|
||||||
"title": "连接您的工具",
|
"title": "连接您的工具",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue