diff --git a/README.md b/README.md index eeecb598d..99baf8111 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ docker compose -f docker-compose-dev.yaml up -d > Make sure you have Python 3.10 or 3.11 installed. 1. Export required environment variables or prepare a `.env` file in the project folder: - - Copy [.env_sample](https://github.com/arc53/DocsGPT/blob/main/application/.env_sample) and create `.env`. + - Copy [.env-template](https://github.com/arc53/DocsGPT/blob/main/application/.env-template) and create `.env`. (check out [`application/core/settings.py`](application/core/settings.py) if you want to see more config options.) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 8e62683e6..37a3f7a15 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -2,11 +2,12 @@ import os import shutil import uuid +import math from bson.binary import Binary, UuidRepresentation from bson.dbref import DBRef from bson.objectid import ObjectId -from flask import Blueprint, jsonify, make_response, request +from flask import Blueprint, jsonify, make_response, request, redirect from flask_restx import inputs, fields, Namespace, Resource from werkzeug.utils import secure_filename @@ -429,12 +430,70 @@ def get(self): @user_ns.route("/api/combine") +class RedirectToSources(Resource): + @api.doc( + description="Redirects /api/combine to /api/sources for backward compatibility" + ) + def get(self): + return redirect("/api/sources", code=301) + + +@user_ns.route("/api/sources/paginated") +class PaginatedSources(Resource): + @api.doc(description="Get document with pagination, sorting and filtering") + def get(self): + user = "local" + sort_field = request.args.get("sort", "date") # Default to 'date' + sort_order = request.args.get("order", "desc") # Default to 'desc' + page = int(request.args.get("page", 1)) # Default to 1 + rows_per_page = int(request.args.get("rows", 10)) # Default to 10 + + # Prepare + query = {"user": user} + total_documents = sources_collection.count_documents(query) + total_pages = max(1, math.ceil(total_documents / rows_per_page)) + sort_order = 1 if sort_order == "asc" else -1 + skip = (page - 1) * rows_per_page + + try: + documents = ( + sources_collection.find(query) + .sort(sort_field, sort_order) + .skip(skip) + .limit(rows_per_page) + ) + + paginated_docs = [] + for doc in documents: + doc_data = { + "id": str(doc["_id"]), + "name": doc.get("name", ""), + "date": doc.get("date", ""), + "model": settings.EMBEDDINGS_NAME, + "location": "local", + "tokens": doc.get("tokens", ""), + "retriever": doc.get("retriever", "classic"), + "syncFrequency": doc.get("sync_frequency", ""), + } + paginated_docs.append(doc_data) + + response = { + "total": total_documents, + "totalPages": total_pages, + "currentPage": page, + "paginated": paginated_docs, + } + return make_response(jsonify(response), 200) + + except Exception as err: + return make_response(jsonify({"success": False, "error": str(err)}), 400) + + +@user_ns.route("/api/sources") class CombinedJson(Resource): @api.doc(description="Provide JSON file with combined available indexes") def get(self): user = "local" - sort_field = request.args.get('sort', 'date') # Default to 'date' - sort_order = request.args.get('order', "desc") # Default to 'desc' data = [ { "name": "default", @@ -447,7 +506,7 @@ def get(self): ] try: - for index in sources_collection.find({"user": user}).sort(sort_field, 1 if sort_order=="asc" else -1): + for index in sources_collection.find({"user": user}).sort("date", -1): data.append( { "id": str(index["_id"]), @@ -485,6 +544,7 @@ def get(self): "retriever": "brave_search", } ) + except Exception as err: return make_response(jsonify({"success": False, "error": str(err)}), 400) @@ -1674,7 +1734,9 @@ class TextToSpeech(Resource): tts_model = api.model( "TextToSpeechModel", { - "text": fields.String(required=True, description="Text to be synthesized as audio"), + "text": fields.String( + required=True, description="Text to be synthesized as audio" + ), }, ) @@ -1686,8 +1748,15 @@ def post(self): try: tts_instance = GoogleTTS() audio_base64, detected_language = tts_instance.text_to_speech(text) - return make_response(jsonify({"success": True,'audio_base64': audio_base64,'lang':detected_language}), 200) + return make_response( + jsonify( + { + "success": True, + "audio_base64": audio_base64, + "lang": detected_language, + } + ), + 200, + ) except Exception as err: return make_response(jsonify({"success": False, "error": str(err)}), 400) - - diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 3591469b1..324d5aa05 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -34,10 +34,12 @@ import { selectSelectedDocs, selectSelectedDocsStatus, selectSourceDocs, + selectPaginatedDocuments, setConversations, setModalStateDeleteConv, setSelectedDocs, setSourceDocs, + setPaginatedDocuments, } from './preferences/preferenceSlice'; import Spinner from './assets/spinner.svg'; import SpinnerDark from './assets/spinner-dark.svg'; @@ -72,6 +74,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { const conversations = useSelector(selectConversations); const modalStateDeleteConv = useSelector(selectModalStateDeleteConv); const conversationId = useSelector(selectConversationId); + const paginatedDocuments = useSelector(selectPaginatedDocuments); const [isDeletingConversation, setIsDeletingConversation] = useState(false); const { isMobile } = useMediaQuery(); @@ -143,9 +146,18 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }) .then((updatedDocs) => { dispatch(setSourceDocs(updatedDocs)); + const updatedPaginatedDocs = paginatedDocuments?.filter( + (document) => document.id !== doc.id, + ); + dispatch( + setPaginatedDocuments(updatedPaginatedDocs || paginatedDocuments), + ); dispatch( setSelectedDocs( - updatedDocs?.find((doc) => doc.name.toLowerCase() === 'default'), + Array.isArray(updatedDocs) && + updatedDocs?.find( + (doc: Doc) => doc.name.toLowerCase() === 'default', + ), ), ); }) diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index 84674049e..4e7112d04 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -1,7 +1,8 @@ const endpoints = { USER: { - DOCS: '/api/combine', + DOCS: '/api/sources', DOCS_CHECK: '/api/docs_check', + DOCS_PAGINATED: '/api/sources/paginated', API_KEYS: '/api/get_api_keys', CREATE_API_KEY: '/api/create_api_key', DELETE_API_KEY: '/api/delete_api_key', diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index 53b38f50c..942318ae4 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -2,8 +2,9 @@ import apiClient from '../client'; import endpoints from '../endpoints'; const userService = { - getDocs: (sort = 'date', order = 'desc'): Promise => - apiClient.get(`${endpoints.USER.DOCS}?sort=${sort}&order=${order}`), + getDocs: (): Promise => apiClient.get(`${endpoints.USER.DOCS}`), + getDocsWithPagination: (query: string): Promise => + apiClient.get(`${endpoints.USER.DOCS_PAGINATED}?${query}`), checkDocs: (data: any): Promise => apiClient.post(endpoints.USER.DOCS_CHECK, data), getAPIKeys: (): Promise => apiClient.get(endpoints.USER.API_KEYS), diff --git a/frontend/src/assets/double-arrow-left.svg b/frontend/src/assets/double-arrow-left.svg new file mode 100644 index 000000000..cab9ff900 --- /dev/null +++ b/frontend/src/assets/double-arrow-left.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/frontend/src/assets/double-arrow-right.svg b/frontend/src/assets/double-arrow-right.svg new file mode 100644 index 000000000..0d5167c20 --- /dev/null +++ b/frontend/src/assets/double-arrow-right.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/frontend/src/assets/single-left-arrow.svg b/frontend/src/assets/single-left-arrow.svg new file mode 100644 index 000000000..f28b25921 --- /dev/null +++ b/frontend/src/assets/single-left-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/single-right-arrow.svg b/frontend/src/assets/single-right-arrow.svg new file mode 100644 index 000000000..85729e570 --- /dev/null +++ b/frontend/src/assets/single-right-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/DocumentPagination.tsx b/frontend/src/components/DocumentPagination.tsx new file mode 100644 index 000000000..b0532362b --- /dev/null +++ b/frontend/src/components/DocumentPagination.tsx @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import SingleArrowLeft from '../assets/single-left-arrow.svg'; +import SingleArrowRight from '../assets/single-right-arrow.svg'; +import DoubleArrowLeft from '../assets/double-arrow-left.svg'; +import DoubleArrowRight from '../assets/double-arrow-right.svg'; + +interface PaginationProps { + currentPage: number; + totalPages: number; + rowsPerPage: number; + onPageChange: (page: number) => void; + onRowsPerPageChange: (rows: number) => void; +} + +const Pagination: React.FC = ({ + currentPage, + totalPages, + rowsPerPage, + onPageChange, + onRowsPerPageChange, +}) => { + const [rowsPerPageOptions] = useState([5, 10, 15, 20]); + + const handlePreviousPage = () => { + if (currentPage > 1) { + onPageChange(currentPage - 1); + } + }; + + const handleNextPage = () => { + if (currentPage < totalPages) { + onPageChange(currentPage + 1); + } + }; + + const handleFirstPage = () => { + onPageChange(1); + }; + + const handleLastPage = () => { + onPageChange(totalPages); + }; + + return ( +
+
+ Rows per page: + +
+ +
+ Page {currentPage} of {totalPages} +
+ +
+ + + + +
+
+ ); +}; + +export default Pagination; diff --git a/frontend/src/hooks/useDefaultDocument.ts b/frontend/src/hooks/useDefaultDocument.ts index 37374ce0b..7f4b98126 100644 --- a/frontend/src/hooks/useDefaultDocument.ts +++ b/frontend/src/hooks/useDefaultDocument.ts @@ -17,11 +17,12 @@ export default function useDefaultDocument() { getDocs().then((data) => { dispatch(setSourceDocs(data)); if (!selectedDoc) - data?.forEach((doc: Doc) => { - if (doc.model && doc.name === 'default') { - dispatch(setSelectedDocs(doc)); - } - }); + Array.isArray(data) && + data?.forEach((doc: Doc) => { + if (doc.model && doc.name === 'default') { + dispatch(setSelectedDocs(doc)); + } + }); }); }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 4319403e7..7c9cc02ce 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -50,11 +50,11 @@ body.dark { @layer components { .table-default { - @apply block w-max table-auto content-center justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray; + @apply block w-full mx-auto table-auto content-start justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray overflow-auto; } .table-default th { - @apply p-4 w-[244px] font-normal text-gray-400; /* Remove border-r */ + @apply p-4 w-full font-normal text-gray-400 text-nowrap; /* Remove border-r */ } .table-default th:last-child { diff --git a/frontend/src/models/misc.ts b/frontend/src/models/misc.ts index 9affd0ab4..0d9c89314 100644 --- a/frontend/src/models/misc.ts +++ b/frontend/src/models/misc.ts @@ -14,6 +14,13 @@ export type Doc = { syncFrequency?: string; }; +export type GetDocsResponse = { + docs: Doc[]; + totalDocuments: number; + totalPages: number; + nextCursor: string; +}; + export type PromptProps = { prompts: { name: string; id: string; type: string }[]; selectedPrompt: { name: string; id: string; type: string }; @@ -22,7 +29,7 @@ export type PromptProps = { }; export type DocumentsProps = { - documents: Doc[] | null; + paginatedDocuments: Doc[] | null; handleDeleteDocument: (index: number, document: Doc) => void; }; diff --git a/frontend/src/preferences/preferenceApi.ts b/frontend/src/preferences/preferenceApi.ts index af1060b83..32cf8b178 100644 --- a/frontend/src/preferences/preferenceApi.ts +++ b/frontend/src/preferences/preferenceApi.ts @@ -1,18 +1,14 @@ import conversationService from '../api/services/conversationService'; import userService from '../api/services/userService'; -import { Doc } from '../models/misc'; +import { Doc, GetDocsResponse } from '../models/misc'; //Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later. -export async function getDocs( - sort = 'date', - order = 'desc', -): Promise { +export async function getDocs(): Promise { try { - const response = await userService.getDocs(sort, order); + const response = await userService.getDocs(); const data = await response.json(); const docs: Doc[] = []; - data.forEach((doc: object) => { docs.push(doc as Doc); }); @@ -24,6 +20,33 @@ export async function getDocs( } } +export async function getDocsWithPagination( + sort = 'date', + order = 'desc', + pageNumber = 1, + rowsPerPage = 10, +): Promise { + try { + const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}`; + const response = await userService.getDocsWithPagination(query); + const data = await response.json(); + const docs: Doc[] = []; + Array.isArray(data.paginated) && + data.paginated.forEach((doc: Doc) => { + docs.push(doc as Doc); + }); + return { + docs: docs, + totalDocuments: data.total, + totalPages: data.totalPages, + nextCursor: data.nextCursor, + }; + } catch (error) { + console.log(error); + return null; + } +} + export async function getConversations(): Promise<{ data: { name: string; id: string }[] | null; loading: boolean; diff --git a/frontend/src/preferences/preferenceSlice.ts b/frontend/src/preferences/preferenceSlice.ts index c566ba70b..8b3064d58 100644 --- a/frontend/src/preferences/preferenceSlice.ts +++ b/frontend/src/preferences/preferenceSlice.ts @@ -20,6 +20,7 @@ export interface Preference { loading: boolean; }; modalState: ActiveState; + paginatedDocuments: Doc[] | null; } const initialState: Preference = { @@ -42,6 +43,7 @@ const initialState: Preference = { loading: false, }, modalState: 'INACTIVE', + paginatedDocuments: null, }; export const prefSlice = createSlice({ @@ -57,6 +59,9 @@ export const prefSlice = createSlice({ setSourceDocs: (state, action) => { state.sourceDocs = action.payload; }, + setPaginatedDocuments: (state, action) => { + state.paginatedDocuments = action.payload; + }, setConversations: (state, action) => { state.conversations = action.payload; }, @@ -84,6 +89,7 @@ export const { setChunks, setTokenLimit, setModalStateDeleteConv, + setPaginatedDocuments, } = prefSlice.actions; export default prefSlice.reducer; @@ -155,3 +161,5 @@ export const selectPrompt = (state: RootState) => state.preference.prompt; export const selectChunks = (state: RootState) => state.preference.chunks; export const selectTokenLimit = (state: RootState) => state.preference.token_limit; +export const selectPaginatedDocuments = (state: RootState) => + state.preference.paginatedDocuments; diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index f94d1a87b..f91a33559 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -1,19 +1,20 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; - import userService from '../api/services/userService'; import SyncIcon from '../assets/sync.svg'; import Trash from '../assets/trash.svg'; import caretSort from '../assets/caret-sort.svg'; import DropdownMenu from '../components/DropdownMenu'; -import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported import SkeletonLoader from '../components/SkeletonLoader'; -import { getDocs } from '../preferences/preferenceApi'; -import { setSourceDocs } from '../preferences/preferenceSlice'; import Input from '../components/Input'; import Upload from '../upload/Upload'; // Import the Upload component +import Pagination from '../components/DocumentPagination'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported +import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; +import { setSourceDocs } from '../preferences/preferenceSlice'; +import { setPaginatedDocuments } from '../preferences/preferenceSlice'; // Utility function to format numbers const formatTokens = (tokens: number): string => { @@ -33,12 +34,11 @@ const formatTokens = (tokens: number): string => { }; const Documents: React.FC = ({ - documents, + paginatedDocuments, handleDeleteDocument, }) => { const { t } = useTranslation(); const dispatch = useDispatch(); - // State for search input const [searchTerm, setSearchTerm] = useState(''); // State for modal: active/inactive @@ -47,48 +47,90 @@ const Documents: React.FC = ({ const [loading, setLoading] = useState(false); const [sortField, setSortField] = useState<'date' | 'tokens'>('date'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + // Pagination + const [currentPage, setCurrentPage] = useState(1); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [totalPages, setTotalPages] = useState(1); + // const [totalDocuments, setTotalDocuments] = useState(0); + // Filter documents based on the search term + const filteredDocuments = paginatedDocuments?.filter((document) => + document.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + // State for documents + const currentDocuments = filteredDocuments ?? []; + console.log('currentDocuments', currentDocuments); const syncOptions = [ { label: 'Never', value: 'never' }, { label: 'Daily', value: 'daily' }, { label: 'Weekly', value: 'weekly' }, { label: 'Monthly', value: 'monthly' }, ]; - const refreshDocs = (field: 'date' | 'tokens') => { - if (field === sortField) { - setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); - } else { - setSortOrder('desc'); - setSortField(field); + + const refreshDocs = ( + field: 'date' | 'tokens' | undefined, + pageNumber?: number, + rows?: number, + ) => { + const page = pageNumber ?? currentPage; + const rowsPerPg = rows ?? rowsPerPage; + + if (field !== undefined) { + if (field === sortField) { + // Toggle sort order + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + // Change sort field and reset order to 'desc' + setSortField(field); + setSortOrder('desc'); + } } - getDocs(sortField, sortOrder) + getDocsWithPagination(sortField, sortOrder, page, rowsPerPg) .then((data) => { - dispatch(setSourceDocs(data)); + //dispatch(setSourceDocs(data ? data.docs : [])); + dispatch(setPaginatedDocuments(data ? data.docs : [])); + setTotalPages(data ? data.totalPages : 0); + //setTotalDocuments(data ? data.totalDocuments : 0); }) .catch((error) => console.error(error)) .finally(() => { setLoading(false); }); }; + const handleManageSync = (doc: Doc, sync_frequency: string) => { setLoading(true); userService .manageSync({ source_id: doc.id, sync_frequency }) .then(() => { + // First, fetch the updated source docs return getDocs(); }) .then((data) => { dispatch(setSourceDocs(data)); + return getDocsWithPagination( + sortField, + sortOrder, + currentPage, + rowsPerPage, + ); }) - .catch((error) => console.error(error)) + .then((paginatedData) => { + dispatch( + setPaginatedDocuments(paginatedData ? paginatedData.docs : []), + ); + setTotalPages(paginatedData ? paginatedData.totalPages : 0); + }) + .catch((error) => console.error('Error in handleManageSync:', error)) .finally(() => { setLoading(false); }); }; - // Filter documents based on the search term - const filteredDocuments = documents?.filter((document) => - document.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); + useEffect(() => { + if (modalState === 'INACTIVE') { + refreshDocs(sortField, currentPage, rowsPerPage); + } + }, [modalState, sortField, currentPage, rowsPerPage]); return (
@@ -154,16 +196,16 @@ const Documents: React.FC = ({ - {!filteredDocuments?.length && ( + {!currentDocuments?.length && ( {t('settings.documents.noData')} )} - {filteredDocuments && - filteredDocuments.map((document, index) => ( - + {Array.isArray(currentDocuments) && + currentDocuments.map((document, index) => ( + {document.name} {document.date} @@ -173,7 +215,7 @@ const Documents: React.FC = ({ {document.type === 'remote' ? 'Pre-loaded' : 'Private'} -
+
{document.type !== 'remote' && ( = ({
)}
+ {/* Pagination component with props: + # Note: Every time the page changes, + the refreshDocs function is called with the updated page number and rows per page. + and reset cursor paginated query parameter to undefined. + */} + { + setCurrentPage(page); + refreshDocs(sortField, page, rowsPerPage); + }} + onRowsPerPageChange={(rows) => { + setRowsPerPage(rows); + setCurrentPage(1); + refreshDocs(sortField, 1, rows); + }} + />
); }; Documents.propTypes = { - documents: PropTypes.array.isRequired, + //documents: PropTypes.array.isRequired, handleDeleteDocument: PropTypes.func.isRequired, }; diff --git a/frontend/src/settings/index.tsx b/frontend/src/settings/index.tsx index ea3d44281..15c7ce086 100644 --- a/frontend/src/settings/index.tsx +++ b/frontend/src/settings/index.tsx @@ -8,6 +8,8 @@ import i18n from '../locale/i18n'; import { Doc } from '../models/misc'; import { selectSourceDocs, + selectPaginatedDocuments, + setPaginatedDocuments, setSourceDocs, } from '../preferences/preferenceSlice'; import Analytics from './Analytics'; @@ -26,20 +28,29 @@ export default function Settings() { ); const documents = useSelector(selectSourceDocs); + const paginatedDocuments = useSelector(selectPaginatedDocuments); const updateWidgetScreenshot = (screenshot: File | null) => { setWidgetScreenshot(screenshot); }; + const updateDocumentsList = (documents: Doc[], index: number) => [ + ...documents.slice(0, index), + ...documents.slice(index + 1), + ]; + const handleDeleteClick = (index: number, doc: Doc) => { userService .deletePath(doc.id ?? '') .then((response) => { if (response.ok && documents) { - const updatedDocuments = [ - ...documents.slice(0, index), - ...documents.slice(index + 1), - ]; - dispatch(setSourceDocs(updatedDocuments)); + if (paginatedDocuments) { + dispatch( + setPaginatedDocuments( + updateDocumentsList(paginatedDocuments, index), + ), + ); + } + dispatch(setSourceDocs(updateDocumentsList(documents, index))); } }) .catch((error) => console.error(error)); @@ -72,7 +83,7 @@ export default function Settings() { case t('settings.documents.label'): return ( ); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 5843d4932..8f426ed61 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -38,6 +38,7 @@ const preloadedState: { preference: Preference } = { }, ], modalState: 'INACTIVE', + paginatedDocuments: null, }, }; const store = configureStore({ diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 01ecc4800..4fee88f6d 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -166,7 +166,10 @@ function Upload({ dispatch(setSourceDocs(data)); dispatch( setSelectedDocs( - data?.find((d) => d.type?.toLowerCase() === 'local'), + Array.isArray(data) && + data?.find( + (d: Doc) => d.type?.toLowerCase() === 'local', + ), ), ); }); @@ -182,15 +185,21 @@ function Upload({ getDocs().then((data) => { dispatch(setSourceDocs(data)); const docIds = new Set( - sourceDocs?.map((doc: Doc) => (doc.id ? doc.id : null)), + (Array.isArray(sourceDocs) && + sourceDocs?.map((doc: Doc) => + doc.id ? doc.id : null, + )) || + [], ); - data?.map((updatedDoc: Doc) => { - if (updatedDoc.id && !docIds.has(updatedDoc.id)) { - //select the doc not present in the intersection of current Docs and fetched data - dispatch(setSelectedDocs(updatedDoc)); - return; - } - }); + if (data && Array.isArray(data)) { + data.map((updatedDoc: Doc) => { + if (updatedDoc.id && !docIds.has(updatedDoc.id)) { + // Select the doc not present in the intersection of current Docs and fetched data + dispatch(setSelectedDocs(updatedDoc)); + return; + } + }); + } }); setProgress( (progress) =>