mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 12:52:39 +02:00
feat(sidebar): add loading skeletons to DocumentsSidebar and LocalFilesystemBrowser during data fetching
This commit is contained in:
parent
f7fa96ccd0
commit
dbdeaa1bcf
2 changed files with 162 additions and 48 deletions
|
|
@ -73,6 +73,7 @@ import {
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
@ -99,6 +100,32 @@ const NON_DELETABLE_DOCUMENT_TYPES: readonly string[] = ["SURFSENSE_DOCS"];
|
||||||
const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
|
const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
|
||||||
const MAX_LOCAL_FILESYSTEM_ROOTS = 5;
|
const MAX_LOCAL_FILESYSTEM_ROOTS = 5;
|
||||||
|
|
||||||
|
function CloudDocumentsSkeleton() {
|
||||||
|
const rows = [
|
||||||
|
{ id: "row-1", widthClass: "w-44" },
|
||||||
|
{ id: "row-2", widthClass: "w-32" },
|
||||||
|
{ id: "row-3", widthClass: "w-32" },
|
||||||
|
{ id: "row-4", widthClass: "w-44" },
|
||||||
|
{ id: "row-5", widthClass: "w-32" },
|
||||||
|
{ id: "row-6", widthClass: "w-32" },
|
||||||
|
{ id: "row-7", widthClass: "w-44" },
|
||||||
|
{ id: "row-8", widthClass: "w-32" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-1">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{rows.map((row) => (
|
||||||
|
<div key={row.id} className="flex h-8 items-center gap-2 px-2">
|
||||||
|
<Skeleton className="h-4 w-4 rounded-sm" />
|
||||||
|
<Skeleton className={`h-4 ${row.widthClass}`} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type FilesystemSettings = {
|
type FilesystemSettings = {
|
||||||
mode: "cloud" | "desktop_local_folder";
|
mode: "cloud" | "desktop_local_folder";
|
||||||
localRootPaths: string[];
|
localRootPaths: string[];
|
||||||
|
|
@ -407,8 +434,8 @@ function AuthenticatedDocumentsSidebar({
|
||||||
);
|
);
|
||||||
|
|
||||||
// Zero queries for tree data
|
// Zero queries for tree data
|
||||||
const [zeroFolders] = useQuery(queries.folders.bySpace({ searchSpaceId }));
|
const [zeroFolders, zeroFoldersResult] = useQuery(queries.folders.bySpace({ searchSpaceId }));
|
||||||
const [zeroAllDocs] = useQuery(queries.documents.bySpace({ searchSpaceId }));
|
const [zeroAllDocs, zeroAllDocsResult] = useQuery(queries.documents.bySpace({ searchSpaceId }));
|
||||||
const [agentCreatedDocs, setAgentCreatedDocs] = useAtom(agentCreatedDocumentsAtom);
|
const [agentCreatedDocs, setAgentCreatedDocs] = useAtom(agentCreatedDocumentsAtom);
|
||||||
|
|
||||||
const treeFolders: FolderDisplay[] = useMemo(
|
const treeFolders: FolderDisplay[] = useMemo(
|
||||||
|
|
@ -989,6 +1016,9 @@ function AuthenticatedDocumentsSidebar({
|
||||||
|
|
||||||
const showFilesystemTabs = !isMobile && !!electronAPI && !!filesystemSettings;
|
const showFilesystemTabs = !isMobile && !!electronAPI && !!filesystemSettings;
|
||||||
const currentFilesystemTab = filesystemSettings?.mode === "desktop_local_folder" ? "local" : "cloud";
|
const currentFilesystemTab = filesystemSettings?.mode === "desktop_local_folder" ? "local" : "cloud";
|
||||||
|
const showCloudSkeleton =
|
||||||
|
currentFilesystemTab === "cloud" &&
|
||||||
|
(zeroFoldersResult.type !== "complete" || zeroAllDocsResult.type !== "complete");
|
||||||
|
|
||||||
const cloudContent = (
|
const cloudContent = (
|
||||||
<>
|
<>
|
||||||
|
|
@ -1101,45 +1131,49 @@ function AuthenticatedDocumentsSidebar({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FolderTreeView
|
{showCloudSkeleton ? (
|
||||||
folders={treeFolders}
|
<CloudDocumentsSkeleton />
|
||||||
documents={searchFilteredDocuments}
|
) : (
|
||||||
expandedIds={expandedIds}
|
<FolderTreeView
|
||||||
onToggleExpand={toggleFolderExpand}
|
folders={treeFolders}
|
||||||
mentionedDocIds={mentionedDocIds}
|
documents={searchFilteredDocuments}
|
||||||
onToggleChatMention={handleToggleChatMention}
|
expandedIds={expandedIds}
|
||||||
onToggleFolderSelect={handleToggleFolderSelect}
|
onToggleExpand={toggleFolderExpand}
|
||||||
onRenameFolder={handleRenameFolder}
|
mentionedDocIds={mentionedDocIds}
|
||||||
onDeleteFolder={handleDeleteFolder}
|
onToggleChatMention={handleToggleChatMention}
|
||||||
onMoveFolder={handleMoveFolder}
|
onToggleFolderSelect={handleToggleFolderSelect}
|
||||||
onCreateFolder={handleCreateFolder}
|
onRenameFolder={handleRenameFolder}
|
||||||
searchQuery={debouncedSearch.trim() || undefined}
|
onDeleteFolder={handleDeleteFolder}
|
||||||
onPreviewDocument={(doc) => {
|
onMoveFolder={handleMoveFolder}
|
||||||
openEditorPanel({
|
onCreateFolder={handleCreateFolder}
|
||||||
documentId: doc.id,
|
searchQuery={debouncedSearch.trim() || undefined}
|
||||||
searchSpaceId,
|
onPreviewDocument={(doc) => {
|
||||||
title: doc.title,
|
openEditorPanel({
|
||||||
});
|
documentId: doc.id,
|
||||||
}}
|
searchSpaceId,
|
||||||
onEditDocument={(doc) => {
|
title: doc.title,
|
||||||
openEditorPanel({
|
});
|
||||||
documentId: doc.id,
|
}}
|
||||||
searchSpaceId,
|
onEditDocument={(doc) => {
|
||||||
title: doc.title,
|
openEditorPanel({
|
||||||
});
|
documentId: doc.id,
|
||||||
}}
|
searchSpaceId,
|
||||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
title: doc.title,
|
||||||
onMoveDocument={handleMoveDocument}
|
});
|
||||||
onExportDocument={handleExportDocument}
|
}}
|
||||||
onVersionHistory={(doc) => setVersionDocId(doc.id)}
|
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||||
activeTypes={activeTypes}
|
onMoveDocument={handleMoveDocument}
|
||||||
onDropIntoFolder={handleDropIntoFolder}
|
onExportDocument={handleExportDocument}
|
||||||
onReorderFolder={handleReorderFolder}
|
onVersionHistory={(doc) => setVersionDocId(doc.id)}
|
||||||
watchedFolderIds={watchedFolderIds}
|
activeTypes={activeTypes}
|
||||||
onRescanFolder={handleRescanFolder}
|
onDropIntoFolder={handleDropIntoFolder}
|
||||||
onStopWatchingFolder={handleStopWatching}
|
onReorderFolder={handleReorderFolder}
|
||||||
onExportFolder={handleExportFolder}
|
watchedFolderIds={watchedFolderIds}
|
||||||
/>
|
onRescanFolder={handleRescanFolder}
|
||||||
|
onStopWatchingFolder={handleStopWatching}
|
||||||
|
onExportFolder={handleExportFolder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronDown, ChevronRight, FileText, Folder } from "lucide-react";
|
import { ChevronDown, ChevronRight, FileText, Folder } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { DEFAULT_EXCLUDE_PATTERNS } from "@/components/sources/FolderWatchDialog";
|
import { DEFAULT_EXCLUDE_PATTERNS } from "@/components/sources/FolderWatchDialog";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useElectronAPI } from "@/hooks/use-platform";
|
import { useElectronAPI } from "@/hooks/use-platform";
|
||||||
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
|
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
|
||||||
|
|
@ -39,6 +40,8 @@ type LocalRootMount = {
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MountLoadStatus = "idle" | "loading" | "complete" | "error";
|
||||||
|
|
||||||
const getFolderDisplayName = (rootPath: string): string =>
|
const getFolderDisplayName = (rootPath: string): string =>
|
||||||
rootPath.split(/[\\/]/).at(-1) || rootPath;
|
rootPath.split(/[\\/]/).at(-1) || rootPath;
|
||||||
|
|
||||||
|
|
@ -79,6 +82,10 @@ export function LocalFilesystemBrowser({
|
||||||
const [rootStateMap, setRootStateMap] = useState<Record<string, RootLoadState>>({});
|
const [rootStateMap, setRootStateMap] = useState<Record<string, RootLoadState>>({});
|
||||||
const [expandedFolderKeys, setExpandedFolderKeys] = useState<Set<string>>(new Set());
|
const [expandedFolderKeys, setExpandedFolderKeys] = useState<Set<string>>(new Set());
|
||||||
const [mountByRootKey, setMountByRootKey] = useState<Map<string, string>>(new Map());
|
const [mountByRootKey, setMountByRootKey] = useState<Map<string, string>>(new Map());
|
||||||
|
const [mountStatus, setMountStatus] = useState<MountLoadStatus>("idle");
|
||||||
|
const [mountRefreshInFlight, setMountRefreshInFlight] = useState(false);
|
||||||
|
const hasLoadedMountsOnceRef = useRef(false);
|
||||||
|
const hasResolvedAtLeastOneRootRef = useRef(false);
|
||||||
const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []);
|
const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []);
|
||||||
const isWindowsPlatform = electronAPI?.versions.platform === "win32";
|
const isWindowsPlatform = electronAPI?.versions.platform === "win32";
|
||||||
|
|
||||||
|
|
@ -139,23 +146,44 @@ export function LocalFilesystemBrowser({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!electronAPI?.getAgentFilesystemMounts) {
|
if (!electronAPI?.getAgentFilesystemMounts) {
|
||||||
|
setMountStatus("error");
|
||||||
setMountByRootKey(new Map());
|
setMountByRootKey(new Map());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
const isInitialMountLoad = !hasLoadedMountsOnceRef.current;
|
||||||
|
if (isInitialMountLoad) {
|
||||||
|
setMountStatus("loading");
|
||||||
|
} else {
|
||||||
|
setMountRefreshInFlight(true);
|
||||||
|
}
|
||||||
void electronAPI
|
void electronAPI
|
||||||
.getAgentFilesystemMounts()
|
.getAgentFilesystemMounts()
|
||||||
.then((mounts: LocalRootMount[]) => {
|
.then((mounts: LocalRootMount[]) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
const knownRootKeys = new Set(
|
||||||
|
rootPaths.map((rootPath) => normalizeRootPathForLookup(rootPath, isWindowsPlatform))
|
||||||
|
);
|
||||||
const next = new Map<string, string>();
|
const next = new Map<string, string>();
|
||||||
for (const entry of mounts) {
|
for (const entry of mounts) {
|
||||||
next.set(normalizeRootPathForLookup(entry.rootPath, isWindowsPlatform), entry.mount);
|
const normalizedRootKey = normalizeRootPathForLookup(entry.rootPath, isWindowsPlatform);
|
||||||
|
if (!knownRootKeys.has(normalizedRootKey)) continue;
|
||||||
|
next.set(normalizedRootKey, entry.mount);
|
||||||
}
|
}
|
||||||
setMountByRootKey(next);
|
setMountByRootKey(next);
|
||||||
|
setMountStatus("complete");
|
||||||
|
hasLoadedMountsOnceRef.current = true;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setMountByRootKey(new Map());
|
if (isInitialMountLoad) {
|
||||||
|
setMountByRootKey(new Map());
|
||||||
|
setMountStatus("error");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setMountRefreshInFlight(false);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|
@ -265,6 +293,43 @@ export function LocalFilesystemBrowser({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allRootsLoaded = rootPaths.every((rootPath) => {
|
||||||
|
const state = rootStateMap[rootPath];
|
||||||
|
return !!state && !state.loading;
|
||||||
|
});
|
||||||
|
const mountsSettled = mountStatus === "complete" || mountStatus === "error";
|
||||||
|
if (allRootsLoaded && mountsSettled && rootPaths.length > 0) {
|
||||||
|
hasResolvedAtLeastOneRootRef.current = true;
|
||||||
|
}
|
||||||
|
const showInitialLoading =
|
||||||
|
!hasResolvedAtLeastOneRootRef.current && (!allRootsLoaded || !mountsSettled);
|
||||||
|
|
||||||
|
if (showInitialLoading) {
|
||||||
|
const rows = [
|
||||||
|
{ id: "local-row-1", widthClass: "w-44" },
|
||||||
|
{ id: "local-row-2", widthClass: "w-32" },
|
||||||
|
{ id: "local-row-3", widthClass: "w-32" },
|
||||||
|
{ id: "local-row-4", widthClass: "w-44" },
|
||||||
|
{ id: "local-row-5", widthClass: "w-32" },
|
||||||
|
{ id: "local-row-6", widthClass: "w-32" },
|
||||||
|
{ id: "local-row-7", widthClass: "w-44" },
|
||||||
|
{ id: "local-row-8", widthClass: "w-32" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{rows.map((row) => (
|
||||||
|
<div key={row.id} className="flex h-8 items-center gap-2 px-2">
|
||||||
|
<Skeleton className="h-4 w-4 rounded-sm" />
|
||||||
|
<Skeleton className={`h-4 ${row.widthClass}`} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-2">
|
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-2">
|
||||||
{treeByRoot.map(({ rootPath, rootNode, matchCount, totalCount }) => {
|
{treeByRoot.map(({ rootPath, rootNode, matchCount, totalCount }) => {
|
||||||
|
|
@ -273,9 +338,11 @@ export function LocalFilesystemBrowser({
|
||||||
const mount = mountByRootKey.get(rootKey);
|
const mount = mountByRootKey.get(rootKey);
|
||||||
if (!state || state.loading) {
|
if (!state || state.loading) {
|
||||||
return (
|
return (
|
||||||
<div key={rootPath} className="flex h-16 items-center gap-2 px-3 text-sm text-muted-foreground">
|
<div key={rootPath} className="mb-1 px-3 py-2 text-xs text-muted-foreground/80">
|
||||||
<Spinner size="sm" />
|
<div className="flex items-center gap-2">
|
||||||
<span>Loading {getFolderDisplayName(rootPath)}...</span>
|
<Spinner className="size-3.5" />
|
||||||
|
<span>Loading {getFolderDisplayName(rootPath)}...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -291,11 +358,24 @@ export function LocalFilesystemBrowser({
|
||||||
return (
|
return (
|
||||||
<div key={rootPath} className="mb-1">
|
<div key={rootPath} className="mb-1">
|
||||||
{mount ? renderFolder(rootNode, 0, mount) : null}
|
{mount ? renderFolder(rootNode, 0, mount) : null}
|
||||||
{!mount && (
|
{!mount && (mountRefreshInFlight || mountStatus === "loading") && (
|
||||||
|
<div className="px-3 pb-2 text-xs text-muted-foreground/80">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Spinner className="size-3.5" />
|
||||||
|
<span>Loading {getFolderDisplayName(rootPath)}...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!mount && mountStatus === "complete" && !mountRefreshInFlight && (
|
||||||
<div className="px-3 pb-2 text-xs text-muted-foreground/80">
|
<div className="px-3 pb-2 text-xs text-muted-foreground/80">
|
||||||
Unable to resolve mounted root for this folder.
|
Unable to resolve mounted root for this folder.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!mount && mountStatus === "error" && (
|
||||||
|
<div className="px-3 pb-2 text-xs text-muted-foreground/80">
|
||||||
|
Failed to resolve local folder mounts.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isEmpty && (
|
{isEmpty && (
|
||||||
<div className="px-3 pb-2 text-xs text-muted-foreground/80">
|
<div className="px-3 pb-2 text-xs text-muted-foreground/80">
|
||||||
No supported files found in this folder.
|
No supported files found in this folder.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue