From eb01aa2b802bb4ed9eea57e1d499d712622c5b3d Mon Sep 17 00:00:00 2001 From: Arjita Mitra Date: Tue, 2 Jun 2026 16:11:39 +0200 Subject: [PATCH] feat: load-more pagination for conversation drive search view Conversation Drive now switches pagination model based on the search view state - Browse mode (search view closed): Page number and rows per page based pagination unchanged - Search mode (search view open, regardless of typed query or filters): Load More button with 30 initial rows + 20 per click, matching the global All Files view's progression. --- apps/webapp/src/i18n/en-US.json | 2 + .../ConversationCells.styles.ts | 19 ++++ .../ConversationCells/ConversationCells.tsx | 81 +++++++++++++---- .../loadMorePagination.test.ts | 42 +++++++++ .../loadMorePagination/loadMorePagination.ts | 40 +++++++++ .../common/useCellsStore/useCellsStore.ts | 21 ++++- .../useConversationSearchFiles.ts | 86 ++++++++++++++++--- .../useGetAllCellsNodes.ts | 15 ++-- .../repositories/cells/cellsRepository.ts | 6 ++ apps/webapp/src/types/i18n.d.ts | 2 + 10 files changed, 279 insertions(+), 35 deletions(-) create mode 100644 apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.test.ts create mode 100644 apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.ts diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index a434345d77c..d18bcdb38ec 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -466,6 +466,8 @@ "cells.options.share": "Share via link", "cells.options.tags": "Add or Remove Tags", "cells.options.versionHistory": "Version History", + "cells.pagination.loadMoreError.heading": "Couldn't load more files", + "cells.pagination.loadMoreError.retry": "Retry", "cells.pagination.loadMoreResults": "Load More Items", "cells.pagination.nextPage": "Next Page", "cells.pagination.previousPage": "Previous Page", diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.styles.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.styles.ts index 19bcee29d90..fa156c1a178 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.styles.ts +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.styles.ts @@ -25,3 +25,22 @@ export const wrapperStyles: CSSObject = { flexDirection: 'column', height: '100%', }; + +export const loadMoreWrapperStyles: CSSObject = { + padding: '20px', + display: 'flex', + justifyContent: 'center', +}; + +export const loadMoreErrorWrapperStyles: CSSObject = { + padding: '20px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '8px', +}; + +export const loadMoreErrorMessageStyles: CSSObject = { + fontSize: '14px', + color: 'var(--text-color-secondary)', +}; diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.tsx b/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.tsx index 9b518fbbca8..904a4f735ec 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.tsx +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/ConversationCells.tsx @@ -21,6 +21,8 @@ import {memo, useCallback, useEffect, useRef} from 'react'; import {CONVERSATION_CELLS_STATE} from '@wireapp/api-client/lib/conversation'; +import {Button, ButtonVariant} from '@wireapp/react-ui-kit'; + import {CellsRepository} from 'Repositories/cells/cellsRepository'; import {ConversationRepository} from 'Repositories/conversation/ConversationRepository'; import {Conversation} from 'Repositories/entity/Conversation'; @@ -33,11 +35,16 @@ import {CellsLoader} from './CellsLoader/CellsLoader'; import {CellsPagination} from './CellsPagination/CellsPagination'; import {CellsStateInfo} from './CellsStateInfo/CellsStateInfo'; import {CellsTable} from './CellsTable/CellsTable'; -import {hasActiveConversationDriveFilters} from './common/driveFilters/driveFilters'; +import {getLoadMoreOffset} from './common/loadMorePagination/loadMorePagination'; import {isInRecycleBin} from './common/recycleBin/recycleBin'; import {useCellsStore} from './common/useCellsStore/useCellsStore'; import {useConversationDriveFilters} from './common/useConversationDriveFilters/useConversationDriveFilters'; -import {wrapperStyles} from './ConversationCells.styles'; +import { + loadMoreErrorMessageStyles, + loadMoreErrorWrapperStyles, + loadMoreWrapperStyles, + wrapperStyles, +} from './ConversationCells.styles'; import {useCellsPagination} from './useCellsPagination/useCellsPagination'; import {useConversationSearchFiles} from './useConversationSearch/useConversationSearchFiles'; import {useGetAllCellsNodes} from './useGetAllCellsNodes/useGetAllCellsNodes'; @@ -67,7 +74,7 @@ export const ConversationCells = memo( const {fireAndForgetInvoker} = useApplicationContext(); const {cellsState: initialCellState, name} = useKoSubscribableChildren(activeConversation, ['cellsState', 'name']); - const {getNodes, status: nodesStatus, getPagination} = useCellsStore(); + const {getNodes, status: nodesStatus, getPagination, error: storeError} = useCellsStore(); const conversationId = activeConversation.id; const conversationQualifiedId = activeConversation.qualifiedId; @@ -94,12 +101,13 @@ export const ConversationCells = memo( cellsRepository, conversationRepository, }); - const hasActiveFilters = hasActiveConversationDriveFilters(filterState); const { searchValue, handleSearch, + handleReload, handleClearSearch: clearSearch, + loadMore: loadMoreSearchResults, } = useConversationSearchFiles({ cellsRepository, conversationQualifiedId, @@ -110,8 +118,9 @@ export const ConversationCells = memo( onClear: refresh, }); - const trimmedSearchValue = searchValue.trim(); - const isSearchActive = !!trimmedSearchValue; + // Search view open ⇒ load-more UI + search-hook data; closed ⇒ page-nav UI + browse-hook data. + // The mode is owned by the view, not by whether the user has typed/filtered yet. + const isInSearchMode = isSearchViewOpen; const wasSearchViewOpen = useRef(isSearchViewOpen); const handleClearSearch = useCallback((): void => { @@ -119,18 +128,27 @@ export const ConversationCells = memo( }, [clearSearch]); useEffect(() => { - if (wasSearchViewOpen.current && !isSearchViewOpen && (isSearchActive || hasActiveFilters)) { + if (wasSearchViewOpen.current && !isSearchViewOpen) { + // Search view just closed — reset any active search/filter and restore the + // browse-mode dataset (handled by clearSearch's onClear callback → refresh). clearAllFilters(); clearSearch({preserveFilters: false}); } wasSearchViewOpen.current = isSearchViewOpen; - }, [clearAllFilters, clearSearch, hasActiveFilters, isSearchActive, isSearchViewOpen]); + }, [clearAllFilters, clearSearch, isSearchViewOpen]); + + const handleRefresh = useCallback((): void => { + if (isInSearchMode) { + fireAndForgetInvoker.fireAndForget(handleReload); + return; + } - // When search is active, refresh should trigger search reload - const handleRefresh = isSearchActive ? () => handleSearch(searchValue) : refresh; + fireAndForgetInvoker.fireAndForget(refresh); + }, [fireAndForgetInvoker, handleReload, isInSearchMode, refresh]); const nodes = getNodes({conversationId}); const pagination = getPagination({conversationId}); + const loadMoreOffset = getLoadMoreOffset(pagination); const {goToPage, getPaginationProps} = useCellsPagination({ pagination, @@ -139,20 +157,36 @@ export const ConversationCells = memo( currentNodesCount: nodes.length, }); - useOnPresignedUrlExpired({conversationId, refreshCallback: refresh}); + const handleLoadMore = useCallback(async (): Promise => { + if (loadMoreOffset === null) { + return; + } + + await loadMoreSearchResults(loadMoreOffset); + }, [loadMoreOffset, loadMoreSearchResults]); + + useOnPresignedUrlExpired({conversationId, refreshCallback: handleRefresh}); const isLoading = nodesStatus === 'loading'; + const isFetchingMore = nodesStatus === 'fetchingMore'; const isError = nodesStatus === 'error'; const isSuccess = nodesStatus === 'success'; const hasNodes = !!nodes.length; const emptyView = !isError && !hasNodes && isCellsStateReady; - const isTableVisible = (isSuccess || isLoading) && isCellsStateReady; + const isTableVisible = (isSuccess || isLoading || isFetchingMore) && isCellsStateReady; const isLoadingVisible = isLoading && isCellsStateReady; - const isNoNodesVisible = !isLoading && emptyView && !isInRecycleBin(); - const isPaginationVisible = !emptyView; - const isEmptyRecycleBin = isInRecycleBin() && emptyView && !isLoading; + const isFetchingMoreVisible = isFetchingMore && isCellsStateReady && hasNodes; + const isNoNodesVisible = !isLoading && !isFetchingMore && emptyView && !isInRecycleBin(); + const isEmptyRecycleBin = isInRecycleBin() && emptyView && !isLoading && !isFetchingMore; + const hasMorePages = loadMoreOffset !== null; + const hasAppendError = isSuccess && hasNodes && storeError !== null; + + const isPaginationVisible = !isInSearchMode && !emptyView; + const isLoadMoreVisible = + isInSearchMode && !isLoading && !isFetchingMore && !emptyView && isSuccess && hasMorePages && !hasAppendError; + const isLoadMoreErrorVisible = isInSearchMode && hasAppendError && hasMorePages; return (
@@ -184,9 +218,24 @@ export const ConversationCells = memo( )} {isEmptyRecycleBin && } - {(isLoadingVisible || isRefreshing) && } + {(isLoadingVisible || isRefreshing || isFetchingMoreVisible) && } {isError && } {isPaginationVisible && } + {isLoadMoreVisible && ( +
+ +
+ )} + {isLoadMoreErrorVisible && ( +
+ {t('cells.pagination.loadMoreError.heading')} + +
+ )}
); }, diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.test.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.test.ts new file mode 100644 index 00000000000..a8451fb6566 --- /dev/null +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.test.ts @@ -0,0 +1,42 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {getLoadMoreOffset} from './loadMorePagination'; + +describe('getLoadMoreOffset', () => { + it('returns null when pagination is missing', () => { + expect(getLoadMoreOffset(null)).toBeNull(); + }); + + it('returns null when next offset is missing', () => { + expect(getLoadMoreOffset({currentPage: 1, totalPages: 2})).toBeNull(); + }); + + it('returns next offset when page count is not available', () => { + expect(getLoadMoreOffset({nextOffset: 30})).toBe(30); + }); + + it('returns next offset when another page is available', () => { + expect(getLoadMoreOffset({nextOffset: 50, currentPage: 1, totalPages: 2})).toBe(50); + }); + + it('returns null when the current page is the last page', () => { + expect(getLoadMoreOffset({nextOffset: 50, currentPage: 2, totalPages: 2})).toBeNull(); + }); +}); diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.ts new file mode 100644 index 00000000000..6ade9b9503f --- /dev/null +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/common/loadMorePagination/loadMorePagination.ts @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CellPagination} from '../cellPagination/cellPagination'; + +export const LOAD_MORE_INITIAL_SIZE = 30; +export const LOAD_MORE_INCREMENT = 20; + +export const getLoadMoreOffset = (pagination: CellPagination | null): number | null => { + const nextOffset = pagination?.nextOffset; + + if (nextOffset === undefined) { + return null; + } + + const currentPage = pagination?.currentPage; + const totalPages = pagination?.totalPages; + + if (currentPage === undefined || totalPages === undefined) { + return nextOffset; + } + + return currentPage < totalPages ? nextOffset : null; +}; diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/common/useCellsStore/useCellsStore.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/common/useCellsStore/useCellsStore.ts index 935b3ff4eb9..cf94785c315 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/common/useCellsStore/useCellsStore.ts +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/common/useCellsStore/useCellsStore.ts @@ -23,7 +23,7 @@ import {CellNode} from 'src/script/types/cellNode'; import {CellPagination} from '../cellPagination/cellPagination'; -type Status = 'idle' | 'loading' | 'success' | 'error'; +type Status = 'idle' | 'loading' | 'fetchingMore' | 'success' | 'error'; const DEFAULT_PAGE_SIZE = 50; @@ -35,6 +35,7 @@ interface CellsState { pageSize: number; setPageSize: (pageSize: number) => void; setNodes: (params: {conversationId: string; nodes: CellNode[]}) => void; + appendNodes: (params: {conversationId: string; nodes: CellNode[]}) => void; setPagination: (params: {conversationId: string; pagination: CellPagination | null}) => void; setStatus: (status: Status) => void; setError: (error: Error | null) => void; @@ -51,6 +52,7 @@ export const useCellsStore = create((set, get) => ({ status: 'idle', error: null, pageSize: DEFAULT_PAGE_SIZE, + setPageSize: pageSize => set({pageSize}), setNodes: ({conversationId, nodes}) => set(state => ({ nodesByConversation: { @@ -58,7 +60,13 @@ export const useCellsStore = create((set, get) => ({ [conversationId]: nodes, }, })), - setPageSize: pageSize => set({pageSize}), + appendNodes: ({conversationId, nodes}) => + set(state => ({ + nodesByConversation: { + ...state.nodesByConversation, + [conversationId]: [...(state.nodesByConversation[conversationId] ?? []), ...nodes], + }, + })), setPagination: ({conversationId, pagination}) => set(state => ({ paginationByConversation: { @@ -93,8 +101,15 @@ export const useCellsStore = create((set, get) => ({ clearAll: ({conversationId}) => { const state = get(); const updatedNodesByConversation = {...state.nodesByConversation}; + const updatedPaginationByConversation = {...state.paginationByConversation}; delete updatedNodesByConversation[conversationId]; - set({nodesByConversation: updatedNodesByConversation, status: 'idle', error: null}); + delete updatedPaginationByConversation[conversationId]; + set({ + nodesByConversation: updatedNodesByConversation, + paginationByConversation: updatedPaginationByConversation, + status: 'idle', + error: null, + }); }, getNodes: ({conversationId}) => { const state = get().nodesByConversation; diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/useConversationSearch/useConversationSearchFiles.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/useConversationSearch/useConversationSearchFiles.ts index b21a3f58253..9558e273f49 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/useConversationSearch/useConversationSearchFiles.ts +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/useConversationSearch/useConversationSearchFiles.ts @@ -34,6 +34,9 @@ import { toConversationDriveSearchParams, } from '../common/driveFilters/driveFilters'; import {getCellsApiPath} from '../common/getCellsApiPath/getCellsApiPath'; +import {getCellsFilesPath} from '../common/getCellsFilesPath/getCellsFilesPath'; +import {LOAD_MORE_INCREMENT, LOAD_MORE_INITIAL_SIZE} from '../common/loadMorePagination/loadMorePagination'; +import {RECYCLE_BIN_PATH} from '../common/recycleBin/recycleBin'; import {useCellsStore} from '../common/useCellsStore/useCellsStore'; import {getUsersFromNodes} from '../useGetAllCellsNodes/getUsersFromNodes'; import {transformDataToCellsNodes, transformToCellPagination} from '../useGetAllCellsNodes/transformDataToCellsNodes'; @@ -60,12 +63,13 @@ export const useConversationSearchFiles = ({ filters, onClear, }: UseConversationSearchFilesProps) => { - const {setNodes, setStatus, setPagination, clearAll} = useCellsStore(); + const {setNodes, appendNodes, setStatus, setPagination, setError, clearAll} = useCellsStore(); const [searchValue, setSearchValue] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const isInitialLoad = useRef(true); const shouldPerformSearch = useRef(false); + const hasFiredInitialFetchRef = useRef(false); const searchParams = useMemo(() => toConversationDriveSearchParams(filters), [filters]); const hasActiveParams = hasActiveSearchParams(searchParams); @@ -75,24 +79,40 @@ export const useConversationSearchFiles = ({ const conversationPath = getCellsApiPath({conversationQualifiedId}); const searchNodes = useCallback( - async ({query, filters: filtersParam}: {query: string; filters: ConversationDriveFiltersState}) => { + async ({ + query, + filters: filtersParam, + offset = 0, + append = false, + }: { + query: string; + filters: ConversationDriveFiltersState; + offset?: number; + append?: boolean; + }) => { try { - setStatus('loading'); + setError(null); + setStatus(append ? 'fetchingMore' : 'loading'); const shouldSort = query.length === 0 || query === FETCH_ALL_QUERY; const searchParams = toConversationDriveSearchParams(filtersParam); const result = await cellsRepository.searchNodes({ query, + limit: append ? LOAD_MORE_INCREMENT : LOAD_MORE_INITIAL_SIZE, + offset, path: conversationPath, sortBy: shouldSort ? 'mtime' : undefined, sortDirection: shouldSort ? 'desc' : undefined, type: 'file', + deleted: getCellsFilesPath() === RECYCLE_BIN_PATH, ...searchParams, }); if (result.Nodes === undefined || result.Nodes.length === 0) { - setNodes({conversationId: id, nodes: []}); + if (!append) { + setNodes({conversationId: id, nodes: []}); + } setPagination({conversationId: id, pagination: null}); setStatus('success'); return; @@ -108,7 +128,11 @@ export const useConversationSearchFiles = ({ users, }); - setNodes({conversationId: id, nodes: transformedNodes}); + if (append) { + appendNodes({conversationId: id, nodes: transformedNodes}); + } else { + setNodes({conversationId: id, nodes: transformedNodes}); + } const pagination = result.Pagination !== undefined ? transformToCellPagination(result.Pagination) : null; setPagination({conversationId: id, pagination}); @@ -118,15 +142,24 @@ export const useConversationSearchFiles = ({ } setStatus('success'); - } catch { + } catch (error) { + const wrappedError = error instanceof Error ? error : new Error('Failed to load files', {cause: error}); + setError(wrappedError); + + if (append) { + // Keep existing list and pagination visible; surface the failure inline via store error. + setStatus('success'); + return; + } + setStatus('error'); setNodes({conversationId: id, nodes: []}); setPagination({conversationId: id, pagination: null}); } }, - // cellsRepository is not a dependency because it's a singleton + // cellsRepository and userRepository are not dependencies because they're singletons // eslint-disable-next-line react-hooks/exhaustive-deps - [setNodes, setPagination, setStatus, id, conversationPath], + [appendNodes, setNodes, setPagination, setStatus, setError, id, conversationPath], ); const searchNodesDebounced = useDebouncedCallback(async (value: string) => { @@ -177,11 +210,11 @@ export const useConversationSearchFiles = ({ onClear?.(); }; - const handleReload = async (): Promise => { + const handleReload = useCallback(async (): Promise => { setStatus('loading'); clearAll({conversationId: id}); await searchNodes({query: searchQuery.trim().length > 0 ? searchQuery : FETCH_ALL_QUERY, filters}); - }; + }, [clearAll, filters, id, searchNodes, searchQuery, setStatus]); useEffect(() => { if (!enabled) { @@ -200,6 +233,26 @@ export const useConversationSearchFiles = ({ }); }, [searchNodes, searchQuery, enabled, filters, hasActiveParams, fireAndForgetInvoker]); + // Fire an initial unfiltered fetch when the hook becomes enabled (search view opens) so + // the load-more dataset is populated even before the user types or selects a filter. + // The ref resets on disable so the next enable cycle (re-opening the search view) refetches. + useEffect(() => { + if (!enabled) { + hasFiredInitialFetchRef.current = false; + return; + } + if (hasFiredInitialFetchRef.current) { + return; + } + hasFiredInitialFetchRef.current = true; + fireAndForgetInvoker.fireAndForget(async (): Promise => { + await searchNodes({ + query: searchQuery.trim().length > 0 ? searchQuery : FETCH_ALL_QUERY, + filters, + }); + }); + }, [enabled, searchNodes, searchQuery, filters, fireAndForgetInvoker]); + // When the search params transition from "active" to "none" with no search query, // restore the default unfiltered file list. useEffect(() => { @@ -209,10 +262,23 @@ export const useConversationSearchFiles = ({ hadActiveSearchParamsRef.current = hasActiveParams; }, [hasActiveParams, searchValue, onClear]); + const loadMore = useCallback( + async (offset: number): Promise => { + await searchNodes({ + query: searchQuery.trim().length > 0 ? searchQuery : FETCH_ALL_QUERY, + filters, + offset, + append: true, + }); + }, + [filters, searchNodes, searchQuery], + ); + return { searchValue, handleSearch, handleReload, handleClearSearch, + loadMore, }; }; diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/useGetAllCellsNodes/useGetAllCellsNodes.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/useGetAllCellsNodes/useGetAllCellsNodes.ts index 8eadefdae8d..d6a8fb45c3a 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/useGetAllCellsNodes/useGetAllCellsNodes.ts +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/useGetAllCellsNodes/useGetAllCellsNodes.ts @@ -17,7 +17,7 @@ * */ -import {useEffect, useCallback, useState} from 'react'; +import {useEffect, useCallback, useMemo, useState} from 'react'; import {QualifiedId} from '@wireapp/api-client/lib/user/'; @@ -52,20 +52,23 @@ export const useGetAllCellsNodes = ({ const {setNodes, pageSize, setStatus, setPagination, setError, clearAll} = useCellsStore(); const [offset, setOffset] = useState(0); - const {id} = conversationQualifiedId; + const {domain, id} = conversationQualifiedId; + const conversationPath = useMemo(() => getCellsApiPath({conversationQualifiedId: {domain, id}}), [domain, id]); const fetchNodes = useCallback(async () => { try { + setError(null); setStatus('loading'); const result = await cellsRepository.getAllNodes({ - path: getCellsApiPath({conversationQualifiedId}), + path: conversationPath, limit: pageSize, offset, deleted: getCellsFilesPath() === RECYCLE_BIN_PATH, }); if (result.Nodes === undefined || result.Nodes.length === 0) { + setNodes({conversationId: id, nodes: []}); setStatus('success'); setPagination({conversationId: id, pagination: null}); return; @@ -89,9 +92,9 @@ export const useGetAllCellsNodes = ({ setStatus('error'); throw error; } - // cellsRepository is not a dependency because it's a singleton + // cellsRepository and userRepository are not dependencies because they're singletons // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setNodes, setStatus, setError, id, offset, pageSize, setPagination]); + }, [conversationPath, id, offset, pageSize, setError, setNodes, setPagination, setStatus]); const handleHashChange = useCallback((): void => { if (enabled !== true) { @@ -100,7 +103,7 @@ export const useGetAllCellsNodes = ({ clearAll({conversationId: id}); setOffset(0); fireAndForgetInvoker.fireAndForget(fetchNodes); - }, [clearAll, enabled, fetchNodes, fireAndForgetInvoker, id, setOffset]); + }, [clearAll, enabled, fetchNodes, fireAndForgetInvoker, id]); useEffect(() => { if (enabled !== true) { diff --git a/apps/webapp/src/script/repositories/cells/cellsRepository.ts b/apps/webapp/src/script/repositories/cells/cellsRepository.ts index d4d8dae8e29..416743b8a74 100644 --- a/apps/webapp/src/script/repositories/cells/cellsRepository.ts +++ b/apps/webapp/src/script/repositories/cells/cellsRepository.ts @@ -240,6 +240,7 @@ export class CellsRepository { async searchNodes({ query, limit = DEFAULT_MAX_FILES_LIMIT, + offset = 0, tags, mimeTypes, hasPublicLink, @@ -248,9 +249,11 @@ export class CellsRepository { type, sortBy, sortDirection, + deleted = false, }: { query: string; limit?: number; + offset?: number; tags?: string[]; mimeTypes?: string[]; hasPublicLink?: boolean; @@ -259,10 +262,12 @@ export class CellsRepository { type?: 'file' | 'folder'; sortBy?: SortBy; sortDirection?: SortDirection; + deleted?: boolean; }) { return this.apiClient.api.cells.searchNodes({ phrase: query, limit, + offset, sortBy, sortDirection, tags, @@ -270,6 +275,7 @@ export class CellsRepository { hasPublicLink, creatorIds, path, + deleted, ...(type ? {type: type === 'file' ? 'LEAF' : 'COLLECTION'} : {}), }); } diff --git a/apps/webapp/src/types/i18n.d.ts b/apps/webapp/src/types/i18n.d.ts index 4f45efda108..80e8da4f6ff 100644 --- a/apps/webapp/src/types/i18n.d.ts +++ b/apps/webapp/src/types/i18n.d.ts @@ -470,6 +470,8 @@ declare module 'I18n/en-US.json' { 'cells.options.share': `Share via link`; 'cells.options.tags': `Add or Remove Tags`; 'cells.options.versionHistory': `Version History`; + 'cells.pagination.loadMoreError.heading': `Couldn\'t load more files`; + 'cells.pagination.loadMoreError.retry': `Retry`; 'cells.pagination.loadMoreResults': `Load More Items`; 'cells.pagination.nextPage': `Next Page`; 'cells.pagination.previousPage': `Previous Page`;