fix(files): show the loading skeleton instead of "No files yet" before the first listing

This commit is contained in:
Linus Rath
2026-10-11 17:00:56 +02:00
parent a4909ae106
commit bf00ea27cf
3 changed files with 18 additions and 4 deletions
@@ -51,7 +51,7 @@ const SHARED = node('e:f-team', 'Team', null, { isShared: true, accountId: 'e',
type Overrides = Partial<React.ComponentProps<typeof FileBrowser>>;
function renderBrowser(opts: { resources?: FileResource[]; allFolders?: FileResource[]; props?: Overrides } = {}) {
function renderBrowser(opts: { resources?: FileResource[]; allFolders?: FileResource[]; props?: Overrides; hasListed?: boolean } = {}) {
const client = {
listAllFileNodesAcrossAccounts: vi.fn(async () => [...OWN, SHARED]),
} as unknown as IJMAPClient;
@@ -62,6 +62,7 @@ function renderBrowser(opts: { resources?: FileResource[]; allFolders?: FileReso
allFolders: opts.allFolders ?? OWN.filter((n) => n.blobId == null).map(resource),
currentParentId: null,
pathStack: [{ id: null, name: '' }],
hasListed: opts.hasListed ?? true,
});
});
const handlers = {
@@ -213,6 +214,12 @@ describe('FileBrowser', () => {
expect(names).toEqual(['c.txt', 'b.txt', 'a.txt']);
});
it('shows the loading skeleton, not the empty state, before the first listing', () => {
renderBrowser({ resources: [], allFolders: [], hasListed: false });
expect(screen.queryByText('empty_state_title')).toBeNull();
expect(screen.queryByText('empty_folder_title')).toBeNull();
});
it('shows an empty state with upload and new folder in an empty folder', () => {
renderBrowser({ resources: [], allFolders: [], props: { currentPath: '/Documents' } });
expect(screen.getByText('empty_folder_title')).toBeInTheDocument();
+3 -1
View File
@@ -187,6 +187,7 @@ export function FileBrowser({
const sharedRoots = useFileStore((s) => s.sharedRoots);
const loadSharedRoots = useFileStore((s) => s.loadSharedRoots);
const allFolders = useFileStore((s) => s.allFolders);
const hasListed = useFileStore((s) => s.hasListed);
const currentParentId = useFileStore((s) => s.currentParentId);
const pathStack = useFileStore((s) => s.pathStack);
useEffect(() => {
@@ -873,7 +874,6 @@ export function FileBrowser({
const showEmpty = !isLoading && !error && displayResources.length === 0 && sharedAtRoot.length === 0;
const listing = (() => {
if (isLoading && resources.length === 0) return <ListSkeleton />;
if (accountPickerMode) {
return accountFolders && accountFolders.length > 0 && onSelectAccount ? (
<div className="p-4">
@@ -903,6 +903,8 @@ export function FileBrowser({
</div>
);
}
// Until the first listing is in, an empty list means "not loaded yet".
if ((isLoading || !hasListed) && resources.length === 0) return <ListSkeleton />;
if (showEmpty && searchQuery) return <NoSearchResults query={searchQuery} onClear={() => setSearchQuery("")} />;
if (showEmpty) {
return (
+7 -2
View File
@@ -80,6 +80,8 @@ interface FileState {
* instead of fetching again, and is current after every change.
*/
allFolders: FileResource[];
/** False until the first listing for the attached account has finished. */
hasListed: boolean;
// Actions
initClient: (client: IJMAPClient, accountId?: string | null) => void;
@@ -296,6 +298,7 @@ export const useFileStore = create<FileState>((set, get) => ({
lastAction: null,
sharedRoots: [],
allFolders: [],
hasListed: false,
favorites: (() => {
try { return JSON.parse(localStorage.getItem('files-favorites') || '[]'); } catch { return []; }
})(),
@@ -319,6 +322,7 @@ export const useFileStore = create<FileState>((set, get) => ({
currentParentId: null,
resources: [],
allFolders: [],
hasListed: false,
selectedResources: new Set<string>(),
error: null,
isLoading: false,
@@ -579,13 +583,14 @@ export const useFileStore = create<FileState>((set, get) => ({
const prunedRecent = recentFiles.filter(r => existingIds.has(r.id));
if (prunedRecent.length !== recentFiles.length) {
try { localStorage.setItem('files-recent-files', JSON.stringify(prunedRecent)); } catch { /* ignore */ }
set({ resources, allFolders, recentFiles: prunedRecent, isLoading: false });
set({ resources, allFolders, recentFiles: prunedRecent, isLoading: false, hasListed: true });
} else {
set({ resources, allFolders, isLoading: false });
set({ resources, allFolders, isLoading: false, hasListed: true });
}
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to list directory',
hasListed: true,
isLoading: false,
resources: [],
});