This commit is contained in:
ManishMadan2882
2024-11-14 04:52:49 +05:30
20 changed files with 425 additions and 86 deletions

View File

@@ -128,7 +128,7 @@ docker compose -f docker-compose-dev.yaml up -d
> Make sure you have Python 3.10 or 3.11 installed. > 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: 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.) (check out [`application/core/settings.py`](application/core/settings.py) if you want to see more config options.)

View File

@@ -2,11 +2,12 @@ import datetime
import os import os
import shutil import shutil
import uuid import uuid
import math
from bson.binary import Binary, UuidRepresentation from bson.binary import Binary, UuidRepresentation
from bson.dbref import DBRef from bson.dbref import DBRef
from bson.objectid import ObjectId 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 flask_restx import inputs, fields, Namespace, Resource
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -429,12 +430,70 @@ class TaskStatus(Resource):
@user_ns.route("/api/combine") @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): class CombinedJson(Resource):
@api.doc(description="Provide JSON file with combined available indexes") @api.doc(description="Provide JSON file with combined available indexes")
def get(self): def get(self):
user = "local" user = "local"
sort_field = request.args.get('sort', 'date') # Default to 'date'
sort_order = request.args.get('order', "desc") # Default to 'desc'
data = [ data = [
{ {
"name": "default", "name": "default",
@@ -447,7 +506,7 @@ class CombinedJson(Resource):
] ]
try: 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( data.append(
{ {
"id": str(index["_id"]), "id": str(index["_id"]),
@@ -485,6 +544,7 @@ class CombinedJson(Resource):
"retriever": "brave_search", "retriever": "brave_search",
} }
) )
except Exception as err: except Exception as err:
return make_response(jsonify({"success": False, "error": str(err)}), 400) return make_response(jsonify({"success": False, "error": str(err)}), 400)
@@ -1674,7 +1734,9 @@ class TextToSpeech(Resource):
tts_model = api.model( tts_model = api.model(
"TextToSpeechModel", "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 @@ class TextToSpeech(Resource):
try: try:
tts_instance = GoogleTTS() tts_instance = GoogleTTS()
audio_base64, detected_language = tts_instance.text_to_speech(text) 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: except Exception as err:
return make_response(jsonify({"success": False, "error": str(err)}), 400) return make_response(jsonify({"success": False, "error": str(err)}), 400)

View File

@@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) {
return ( return (
<> <>
<Component {...pageProps} /> <Component {...pageProps} />
<DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" theme="dark" /> <DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" theme="dark" size="medium" />
</> </>
) )
} }

View File

@@ -34,10 +34,12 @@ import {
selectSelectedDocs, selectSelectedDocs,
selectSelectedDocsStatus, selectSelectedDocsStatus,
selectSourceDocs, selectSourceDocs,
selectPaginatedDocuments,
setConversations, setConversations,
setModalStateDeleteConv, setModalStateDeleteConv,
setSelectedDocs, setSelectedDocs,
setSourceDocs, setSourceDocs,
setPaginatedDocuments,
} from './preferences/preferenceSlice'; } from './preferences/preferenceSlice';
import Spinner from './assets/spinner.svg'; import Spinner from './assets/spinner.svg';
import SpinnerDark from './assets/spinner-dark.svg'; import SpinnerDark from './assets/spinner-dark.svg';
@@ -72,6 +74,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const conversations = useSelector(selectConversations); const conversations = useSelector(selectConversations);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv); const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const conversationId = useSelector(selectConversationId); const conversationId = useSelector(selectConversationId);
const paginatedDocuments = useSelector(selectPaginatedDocuments);
const [isDeletingConversation, setIsDeletingConversation] = useState(false); const [isDeletingConversation, setIsDeletingConversation] = useState(false);
const { isMobile } = useMediaQuery(); const { isMobile } = useMediaQuery();
@@ -143,9 +146,18 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
}) })
.then((updatedDocs) => { .then((updatedDocs) => {
dispatch(setSourceDocs(updatedDocs)); dispatch(setSourceDocs(updatedDocs));
const updatedPaginatedDocs = paginatedDocuments?.filter(
(document) => document.id !== doc.id,
);
dispatch(
setPaginatedDocuments(updatedPaginatedDocs || paginatedDocuments),
);
dispatch( dispatch(
setSelectedDocs( setSelectedDocs(
updatedDocs?.find((doc) => doc.name.toLowerCase() === 'default'), Array.isArray(updatedDocs) &&
updatedDocs?.find(
(doc: Doc) => doc.name.toLowerCase() === 'default',
),
), ),
); );
}) })

View File

@@ -1,7 +1,8 @@
const endpoints = { const endpoints = {
USER: { USER: {
DOCS: '/api/combine', DOCS: '/api/sources',
DOCS_CHECK: '/api/docs_check', DOCS_CHECK: '/api/docs_check',
DOCS_PAGINATED: '/api/sources/paginated',
API_KEYS: '/api/get_api_keys', API_KEYS: '/api/get_api_keys',
CREATE_API_KEY: '/api/create_api_key', CREATE_API_KEY: '/api/create_api_key',
DELETE_API_KEY: '/api/delete_api_key', DELETE_API_KEY: '/api/delete_api_key',

View File

@@ -2,8 +2,9 @@ import apiClient from '../client';
import endpoints from '../endpoints'; import endpoints from '../endpoints';
const userService = { const userService = {
getDocs: (sort = 'date', order = 'desc'): Promise<any> => getDocs: (): Promise<any> => apiClient.get(`${endpoints.USER.DOCS}`),
apiClient.get(`${endpoints.USER.DOCS}?sort=${sort}&order=${order}`), getDocsWithPagination: (query: string): Promise<any> =>
apiClient.get(`${endpoints.USER.DOCS_PAGINATED}?${query}`),
checkDocs: (data: any): Promise<any> => checkDocs: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.DOCS_CHECK, data), apiClient.post(endpoints.USER.DOCS_CHECK, data),
getAPIKeys: (): Promise<any> => apiClient.get(endpoints.USER.API_KEYS), getAPIKeys: (): Promise<any> => apiClient.get(endpoints.USER.API_KEYS),

View File

@@ -0,0 +1,5 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41 10.59L2.83 6L7.41 1.41L6 0L0 6L6 12L7.41 10.59Z" fill="black" fill-opacity="0.54" />
<path d="M15.41 10.59L10.83 6L15.41 1.41L14 0L8 6L14 12L15.41 10.59Z" fill="black"
fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.59 10.59L13.17 6L8.59 1.41L10 0L16 6L10 12L8.59 10.59Z" fill="black"
fill-opacity="0.54" />
<path d="M0.59 10.59L5.17 6L0.59 1.41L2 0L8 6L2 12L0.59 10.59Z" fill="black" fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,3 @@
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41 10.59L2.83 6L7.41 1.41L6 0L0 6L6 12L7.41 10.59Z" fill="black" fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@@ -0,0 +1,3 @@
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.59 1.41L5.17 6L0.59 10.59L2 12L8 6L2 0L0.59 1.41Z" fill="black" fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 203 B

View File

@@ -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<PaginationProps> = ({
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 (
<div className="flex items-center text-xs justify-end gap-4 mt-2 p-2 border-gray-200">
<div className="flex items-center gap-2 ">
<span className="text-gray-900 dark:text-gray-50">Rows per page:</span>
<select
value={rowsPerPage}
onChange={(e) => onRowsPerPageChange(Number(e.target.value))}
className="border border-gray-300 rounded px-2 py-1 dark:bg-dark-charcoal dark:text-gray-50"
>
{rowsPerPageOptions.map((option) => (
<option
className="bg-white dark:bg-dark-charcoal dark:text-gray-50"
key={option}
value={option}
>
{option}
</option>
))}
</select>
</div>
<div className="text-gray-900 dark:text-gray-50">
Page {currentPage} of {totalPages}
</div>
<div className="flex items-center gap-2 text-gray-900 dark:text-gray-50">
<button
onClick={handleFirstPage}
disabled={currentPage === 1}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={DoubleArrowLeft}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
<button
onClick={handlePreviousPage}
disabled={currentPage === 1}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={SingleArrowLeft}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
<button
onClick={handleNextPage}
disabled={currentPage === totalPages}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={SingleArrowRight}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
<button
onClick={handleLastPage}
disabled={currentPage === totalPages}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={DoubleArrowRight}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
</div>
</div>
);
};
export default Pagination;

View File

@@ -17,6 +17,7 @@ export default function useDefaultDocument() {
getDocs().then((data) => { getDocs().then((data) => {
dispatch(setSourceDocs(data)); dispatch(setSourceDocs(data));
if (!selectedDoc) if (!selectedDoc)
Array.isArray(data) &&
data?.forEach((doc: Doc) => { data?.forEach((doc: Doc) => {
if (doc.model && doc.name === 'default') { if (doc.model && doc.name === 'default') {
dispatch(setSelectedDocs(doc)); dispatch(setSelectedDocs(doc));

View File

@@ -50,11 +50,11 @@ body.dark {
@layer components { @layer components {
.table-default { .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 { .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 { .table-default th:last-child {

View File

@@ -14,6 +14,13 @@ export type Doc = {
syncFrequency?: string; syncFrequency?: string;
}; };
export type GetDocsResponse = {
docs: Doc[];
totalDocuments: number;
totalPages: number;
nextCursor: string;
};
export type PromptProps = { export type PromptProps = {
prompts: { name: string; id: string; type: string }[]; prompts: { name: string; id: string; type: string }[];
selectedPrompt: { name: string; id: string; type: string }; selectedPrompt: { name: string; id: string; type: string };
@@ -22,7 +29,7 @@ export type PromptProps = {
}; };
export type DocumentsProps = { export type DocumentsProps = {
documents: Doc[] | null; paginatedDocuments: Doc[] | null;
handleDeleteDocument: (index: number, document: Doc) => void; handleDeleteDocument: (index: number, document: Doc) => void;
}; };

View File

@@ -1,18 +1,14 @@
import conversationService from '../api/services/conversationService'; import conversationService from '../api/services/conversationService';
import userService from '../api/services/userService'; 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. //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( export async function getDocs(): Promise<Doc[] | null> {
sort = 'date',
order = 'desc',
): Promise<Doc[] | null> {
try { try {
const response = await userService.getDocs(sort, order); const response = await userService.getDocs();
const data = await response.json(); const data = await response.json();
const docs: Doc[] = []; const docs: Doc[] = [];
data.forEach((doc: object) => { data.forEach((doc: object) => {
docs.push(doc as Doc); 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<GetDocsResponse | null> {
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<{ export async function getConversations(): Promise<{
data: { name: string; id: string }[] | null; data: { name: string; id: string }[] | null;
loading: boolean; loading: boolean;

View File

@@ -20,6 +20,7 @@ export interface Preference {
loading: boolean; loading: boolean;
}; };
modalState: ActiveState; modalState: ActiveState;
paginatedDocuments: Doc[] | null;
} }
const initialState: Preference = { const initialState: Preference = {
@@ -42,6 +43,7 @@ const initialState: Preference = {
loading: false, loading: false,
}, },
modalState: 'INACTIVE', modalState: 'INACTIVE',
paginatedDocuments: null,
}; };
export const prefSlice = createSlice({ export const prefSlice = createSlice({
@@ -57,6 +59,9 @@ export const prefSlice = createSlice({
setSourceDocs: (state, action) => { setSourceDocs: (state, action) => {
state.sourceDocs = action.payload; state.sourceDocs = action.payload;
}, },
setPaginatedDocuments: (state, action) => {
state.paginatedDocuments = action.payload;
},
setConversations: (state, action) => { setConversations: (state, action) => {
state.conversations = action.payload; state.conversations = action.payload;
}, },
@@ -84,6 +89,7 @@ export const {
setChunks, setChunks,
setTokenLimit, setTokenLimit,
setModalStateDeleteConv, setModalStateDeleteConv,
setPaginatedDocuments,
} = prefSlice.actions; } = prefSlice.actions;
export default prefSlice.reducer; 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 selectChunks = (state: RootState) => state.preference.chunks;
export const selectTokenLimit = (state: RootState) => export const selectTokenLimit = (state: RootState) =>
state.preference.token_limit; state.preference.token_limit;
export const selectPaginatedDocuments = (state: RootState) =>
state.preference.paginatedDocuments;

View File

@@ -1,19 +1,20 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import userService from '../api/services/userService'; import userService from '../api/services/userService';
import SyncIcon from '../assets/sync.svg'; import SyncIcon from '../assets/sync.svg';
import Trash from '../assets/trash.svg'; import Trash from '../assets/trash.svg';
import caretSort from '../assets/caret-sort.svg'; import caretSort from '../assets/caret-sort.svg';
import DropdownMenu from '../components/DropdownMenu'; import DropdownMenu from '../components/DropdownMenu';
import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported
import SkeletonLoader from '../components/SkeletonLoader'; import SkeletonLoader from '../components/SkeletonLoader';
import { getDocs } from '../preferences/preferenceApi';
import { setSourceDocs } from '../preferences/preferenceSlice';
import Input from '../components/Input'; import Input from '../components/Input';
import Upload from '../upload/Upload'; // Import the Upload component 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 // Utility function to format numbers
const formatTokens = (tokens: number): string => { const formatTokens = (tokens: number): string => {
@@ -33,12 +34,11 @@ const formatTokens = (tokens: number): string => {
}; };
const Documents: React.FC<DocumentsProps> = ({ const Documents: React.FC<DocumentsProps> = ({
documents, paginatedDocuments,
handleDeleteDocument, handleDeleteDocument,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
// State for search input // State for search input
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
// State for modal: active/inactive // State for modal: active/inactive
@@ -47,37 +47,49 @@ const Documents: React.FC<DocumentsProps> = ({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sortField, setSortField] = useState<'date' | 'tokens'>('date'); const [sortField, setSortField] = useState<'date' | 'tokens'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
// Pagination
const [currentPage, setCurrentPage] = useState<number>(1);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [totalPages, setTotalPages] = useState<number>(1);
// const [totalDocuments, setTotalDocuments] = useState<number>(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 = [ const syncOptions = [
{ label: 'Never', value: 'never' }, { label: 'Never', value: 'never' },
{ label: 'Daily', value: 'daily' }, { label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' }, { label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' }, { label: 'Monthly', value: 'monthly' },
]; ];
const refreshDocs = (field: 'date' | 'tokens') => {
const refreshDocs = (
field: 'date' | 'tokens' | undefined,
pageNumber?: number,
rows?: number,
) => {
const page = pageNumber ?? currentPage;
const rowsPerPg = rows ?? rowsPerPage;
if (field !== undefined) {
if (field === sortField) { if (field === sortField) {
// Toggle sort order
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else { } else {
setSortOrder('desc'); // Change sort field and reset order to 'desc'
setSortField(field); setSortField(field);
setSortOrder('desc');
} }
getDocs(sortField, sortOrder) }
getDocsWithPagination(sortField, sortOrder, page, rowsPerPg)
.then((data) => { .then((data) => {
dispatch(setSourceDocs(data)); //dispatch(setSourceDocs(data ? data.docs : []));
}) dispatch(setPaginatedDocuments(data ? data.docs : []));
.catch((error) => console.error(error)) setTotalPages(data ? data.totalPages : 0);
.finally(() => { //setTotalDocuments(data ? data.totalDocuments : 0);
setLoading(false);
});
};
const handleManageSync = (doc: Doc, sync_frequency: string) => {
setLoading(true);
userService
.manageSync({ source_id: doc.id, sync_frequency })
.then(() => {
return getDocs();
})
.then((data) => {
dispatch(setSourceDocs(data));
}) })
.catch((error) => console.error(error)) .catch((error) => console.error(error))
.finally(() => { .finally(() => {
@@ -85,10 +97,40 @@ const Documents: React.FC<DocumentsProps> = ({
}); });
}; };
// Filter documents based on the search term const handleManageSync = (doc: Doc, sync_frequency: string) => {
const filteredDocuments = documents?.filter((document) => setLoading(true);
document.name.toLowerCase().includes(searchTerm.toLowerCase()), 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,
); );
})
.then((paginatedData) => {
dispatch(
setPaginatedDocuments(paginatedData ? paginatedData.docs : []),
);
setTotalPages(paginatedData ? paginatedData.totalPages : 0);
})
.catch((error) => console.error('Error in handleManageSync:', error))
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
if (modalState === 'INACTIVE') {
refreshDocs(sortField, currentPage, rowsPerPage);
}
}, [modalState, sortField, currentPage, rowsPerPage]);
return ( return (
<div className="mt-8"> <div className="mt-8">
@@ -154,16 +196,16 @@ const Documents: React.FC<DocumentsProps> = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{!filteredDocuments?.length && ( {!currentDocuments?.length && (
<tr> <tr>
<td colSpan={5} className="!p-4"> <td colSpan={5} className="!p-4">
{t('settings.documents.noData')} {t('settings.documents.noData')}
</td> </td>
</tr> </tr>
)} )}
{filteredDocuments && {Array.isArray(currentDocuments) &&
filteredDocuments.map((document, index) => ( currentDocuments.map((document, index) => (
<tr key={index}> <tr key={index} className="text-nowrap font-normal">
<td>{document.name}</td> <td>{document.name}</td>
<td>{document.date}</td> <td>{document.date}</td>
<td> <td>
@@ -173,7 +215,7 @@ const Documents: React.FC<DocumentsProps> = ({
{document.type === 'remote' ? 'Pre-loaded' : 'Private'} {document.type === 'remote' ? 'Pre-loaded' : 'Private'}
</td> </td>
<td> <td>
<div className="flex flex-row items-center"> <div className="min-w-[70px] flex flex-row items-end justify-end ml-auto">
{document.type !== 'remote' && ( {document.type !== 'remote' && (
<img <img
src={Trash} src={Trash}
@@ -221,12 +263,31 @@ const Documents: React.FC<DocumentsProps> = ({
</div> </div>
)} )}
</div> </div>
{/* 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.
*/}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
rowsPerPage={rowsPerPage}
onPageChange={(page) => {
setCurrentPage(page);
refreshDocs(sortField, page, rowsPerPage);
}}
onRowsPerPageChange={(rows) => {
setRowsPerPage(rows);
setCurrentPage(1);
refreshDocs(sortField, 1, rows);
}}
/>
</div> </div>
); );
}; };
Documents.propTypes = { Documents.propTypes = {
documents: PropTypes.array.isRequired, //documents: PropTypes.array.isRequired,
handleDeleteDocument: PropTypes.func.isRequired, handleDeleteDocument: PropTypes.func.isRequired,
}; };

View File

@@ -8,6 +8,8 @@ import i18n from '../locale/i18n';
import { Doc } from '../models/misc'; import { Doc } from '../models/misc';
import { import {
selectSourceDocs, selectSourceDocs,
selectPaginatedDocuments,
setPaginatedDocuments,
setSourceDocs, setSourceDocs,
} from '../preferences/preferenceSlice'; } from '../preferences/preferenceSlice';
import Analytics from './Analytics'; import Analytics from './Analytics';
@@ -26,20 +28,29 @@ export default function Settings() {
); );
const documents = useSelector(selectSourceDocs); const documents = useSelector(selectSourceDocs);
const paginatedDocuments = useSelector(selectPaginatedDocuments);
const updateWidgetScreenshot = (screenshot: File | null) => { const updateWidgetScreenshot = (screenshot: File | null) => {
setWidgetScreenshot(screenshot); setWidgetScreenshot(screenshot);
}; };
const updateDocumentsList = (documents: Doc[], index: number) => [
...documents.slice(0, index),
...documents.slice(index + 1),
];
const handleDeleteClick = (index: number, doc: Doc) => { const handleDeleteClick = (index: number, doc: Doc) => {
userService userService
.deletePath(doc.id ?? '') .deletePath(doc.id ?? '')
.then((response) => { .then((response) => {
if (response.ok && documents) { if (response.ok && documents) {
const updatedDocuments = [ if (paginatedDocuments) {
...documents.slice(0, index), dispatch(
...documents.slice(index + 1), setPaginatedDocuments(
]; updateDocumentsList(paginatedDocuments, index),
dispatch(setSourceDocs(updatedDocuments)); ),
);
}
dispatch(setSourceDocs(updateDocumentsList(documents, index)));
} }
}) })
.catch((error) => console.error(error)); .catch((error) => console.error(error));
@@ -72,7 +83,7 @@ export default function Settings() {
case t('settings.documents.label'): case t('settings.documents.label'):
return ( return (
<Documents <Documents
documents={documents} paginatedDocuments={paginatedDocuments}
handleDeleteDocument={handleDeleteClick} handleDeleteDocument={handleDeleteClick}
/> />
); );

View File

@@ -38,6 +38,7 @@ const preloadedState: { preference: Preference } = {
}, },
], ],
modalState: 'INACTIVE', modalState: 'INACTIVE',
paginatedDocuments: null,
}, },
}; };
const store = configureStore({ const store = configureStore({

View File

@@ -166,7 +166,10 @@ function Upload({
dispatch(setSourceDocs(data)); dispatch(setSourceDocs(data));
dispatch( dispatch(
setSelectedDocs( 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) => { getDocs().then((data) => {
dispatch(setSourceDocs(data)); dispatch(setSourceDocs(data));
const docIds = new Set( 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 (data && Array.isArray(data)) {
data.map((updatedDoc: Doc) => {
if (updatedDoc.id && !docIds.has(updatedDoc.id)) { if (updatedDoc.id && !docIds.has(updatedDoc.id)) {
//select the doc not present in the intersection of current Docs and fetched data // Select the doc not present in the intersection of current Docs and fetched data
dispatch(setSelectedDocs(updatedDoc)); dispatch(setSelectedDocs(updatedDoc));
return; return;
} }
}); });
}
}); });
setProgress( setProgress(
(progress) => (progress) =>