feat(items): re-enable lazy loading with cursor

example: if a folder has many resource, client will use lazy loading
    and load next cursor if scroll reached the bottom of the page
    purpose: reduce the amount of call to server
This commit is contained in:
Edouard Vanbelle
2026-07-20 21:44:46 +02:00
parent 5b8fb68b30
commit ae6e3a8eb3
4 changed files with 265 additions and 459 deletions
+17 -26
View File
@@ -46,12 +46,10 @@ vi.mock('$lib/api/endpoints/files', () => ({
uploadFileWithProgress: vi.fn()
}));
vi.mock('$lib/api/endpoints/folders', () => ({
cacheFolder: vi.fn(),
createFolder: vi.fn(),
deleteFolder: vi.fn(),
fetchFolderListing: vi.fn(),
fetchFolderPage: vi.fn(),
folderZipUrl: () => '/zip',
getCachedFolder: () => undefined,
getFolder: vi.fn(async (id: string) => ({ id, name: id })),
getFolderName: () => undefined,
invalidateFolderCache: vi.fn(),
@@ -60,7 +58,7 @@ vi.mock('$lib/api/endpoints/folders', () => ({
renameFolder: vi.fn()
}));
import { fetchFolderListing, createFolder, deleteFolder } from '$lib/api/endpoints/folders';
import { fetchFolderPage, createFolder, deleteFolder } from '$lib/api/endpoints/folders';
import { deleteFile } from '$lib/api/endpoints/files';
import { apiFetch } from '$lib/api/client';
import { files as filesStore } from '$lib/stores/files.svelte';
@@ -69,15 +67,17 @@ import FilesPage from './[...path]/+page.svelte';
const m = (fn: unknown) => fn as ReturnType<typeof vi.fn>;
function withListing() {
m(fetchFolderListing).mockResolvedValue({
status: 200,
etag: 'v1',
listing: {
folders: [folderItem('sub1', 'Sub')],
files: [fileItem('f1', 'hello.txt')],
favoriteIds: [],
sharedIds: []
}
// `fetchFolderPage` returns ONE page with the accumulator shape (items in
// server order + folders/files splits). With `nextCursor` omitted the
// caller treats it as the last page — the page's items become the whole
// on-screen listing without triggering `loadMore`.
const folder = folderItem('sub1', 'Sub');
const file = fileItem('f1', 'hello.txt');
m(fetchFolderPage).mockResolvedValue({
items: [folder, file],
folders: [folder],
files: [file],
nextCursor: undefined
});
}
@@ -131,27 +131,18 @@ beforeEach(() => {
});
it('loads the home folder listing on mount and renders its contents', async () => {
m(fetchFolderListing).mockResolvedValue({
status: 200,
etag: 'v1',
listing: {
folders: [folderItem('sub1', 'Sub')],
files: [fileItem('f1', 'hello.txt')],
favoriteIds: [],
sharedIds: []
}
});
withListing();
render(FilesPage);
await waitFor(() => expect(fetchFolderListing).toHaveBeenCalledWith('home', expect.anything()));
await waitFor(() => expect(fetchFolderPage).toHaveBeenCalledWith('home', expect.anything()));
// VirtualList windows rows by viewport height (0 in jsdom), so assert the
// surrounding chrome rendered rather than the windowed rows themselves.
await screen.findByTestId('files-new-folder-btn');
});
it('shows an error when the listing fails with no cache', async () => {
m(fetchFolderListing).mockRejectedValue(Object.assign(new Error('nope'), { status: 500 }));
m(fetchFolderPage).mockRejectedValue(Object.assign(new Error('nope'), { status: 500 }));
render(FilesPage);
await waitFor(() => expect(fetchFolderListing).toHaveBeenCalled());
await waitFor(() => expect(fetchFolderPage).toHaveBeenCalled());
});
it('redirects external users away from the home folder', async () => {