mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
refactor: remove Local Folder connector references and enhance folder management features
This commit is contained in:
parent
5d6e3ffb7b
commit
493d720b89
7 changed files with 257 additions and 132 deletions
|
|
@ -184,14 +184,6 @@ export const OTHER_CONNECTORS = [
|
|||
connectorType: EnumConnectorName.OBSIDIAN_CONNECTOR,
|
||||
selfHostedOnly: true,
|
||||
},
|
||||
{
|
||||
id: "local-folder-connector",
|
||||
title: "Local Folder",
|
||||
description: "Watch and sync local folders (desktop only)",
|
||||
connectorType: EnumConnectorName.LOCAL_FOLDER_CONNECTOR,
|
||||
selfHostedOnly: true,
|
||||
desktopOnly: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
// Composio Connectors - Individual entries for each supported toolkit
|
||||
|
|
|
|||
|
|
@ -586,23 +586,6 @@ export const useConnectorDialog = () => {
|
|||
},
|
||||
});
|
||||
|
||||
// Register folder watcher in Electron for real-time sync
|
||||
if (
|
||||
currentConnectorType === EnumConnectorName.LOCAL_FOLDER_CONNECTOR &&
|
||||
window.electronAPI?.addWatchedFolder
|
||||
) {
|
||||
const cfg = connector.config || {};
|
||||
await window.electronAPI.addWatchedFolder({
|
||||
path: cfg.folder_path as string,
|
||||
name: cfg.folder_name as string,
|
||||
excludePatterns: (cfg.exclude_patterns as string[]) || [],
|
||||
fileExtensions: (cfg.file_extensions as string[] | null) ?? null,
|
||||
connectorId: connector.id,
|
||||
searchSpaceId: Number(searchSpaceId),
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
const successMessage =
|
||||
currentConnectorType === "MCP_CONNECTOR"
|
||||
? `${connector.name} added successfully`
|
||||
|
|
@ -1207,17 +1190,6 @@ export const useConnectorDialog = () => {
|
|||
id: editingConnector.id,
|
||||
});
|
||||
|
||||
// Unregister folder watcher in Electron when removing a Local Folder connector
|
||||
if (
|
||||
editingConnector.connector_type === EnumConnectorName.LOCAL_FOLDER_CONNECTOR &&
|
||||
window.electronAPI?.removeWatchedFolder &&
|
||||
editingConnector.config?.folder_path
|
||||
) {
|
||||
await window.electronAPI.removeWatchedFolder(
|
||||
editingConnector.config.folder_path as string
|
||||
);
|
||||
}
|
||||
|
||||
// Track connector deleted event
|
||||
trackConnectorDeleted(
|
||||
Number(searchSpaceId),
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
|
|||
BOOKSTACK_CONNECTOR: "BOOKSTACK_CONNECTOR",
|
||||
CIRCLEBACK_CONNECTOR: "CIRCLEBACK",
|
||||
OBSIDIAN_CONNECTOR: "OBSIDIAN_CONNECTOR",
|
||||
LOCAL_FOLDER_CONNECTOR: "LOCAL_FOLDER_FILE",
|
||||
|
||||
// Special mappings (connector type differs from document type)
|
||||
GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE",
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@
|
|||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderPlus,
|
||||
MoreHorizontal,
|
||||
Move,
|
||||
PenLine,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
|
@ -70,6 +73,9 @@ interface FolderNodeProps {
|
|||
disabledDropIds?: Set<number>;
|
||||
contextMenuOpen?: boolean;
|
||||
onContextMenuOpenChange?: (open: boolean) => void;
|
||||
isWatched?: boolean;
|
||||
onRescan?: (folder: FolderDisplay) => void;
|
||||
onStopWatching?: (folder: FolderDisplay) => void;
|
||||
}
|
||||
|
||||
function getDropZone(
|
||||
|
|
@ -107,6 +113,9 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
disabledDropIds,
|
||||
contextMenuOpen,
|
||||
onContextMenuOpenChange,
|
||||
isWatched,
|
||||
onRescan,
|
||||
onStopWatching,
|
||||
}: FolderNodeProps) {
|
||||
const [renameValue, setRenameValue] = useState(folder.name);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -307,73 +316,107 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
{isWatched && onRescan && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCreateSubfolder(folder.id);
|
||||
onRescan(folder);
|
||||
}}
|
||||
>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Re-scan
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isWatched && onStopWatching && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startRename();
|
||||
onStopWatching(folder);
|
||||
}}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
<EyeOff className="mr-2 h-4 w-4" />
|
||||
Stop watching
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMove(folder);
|
||||
}}
|
||||
>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(folder);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCreateSubfolder(folder.id);
|
||||
}}
|
||||
>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startRename();
|
||||
}}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMove(folder);
|
||||
}}
|
||||
>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(folder);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
{!isRenaming && contextMenuOpen && (
|
||||
<ContextMenuContent className="w-40">
|
||||
<ContextMenuItem onClick={() => onCreateSubfolder(folder.id)}>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
{!isRenaming && contextMenuOpen && (
|
||||
<ContextMenuContent className="w-40">
|
||||
{isWatched && onRescan && (
|
||||
<ContextMenuItem onClick={() => onRescan(folder)}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Re-scan
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => startRename()}>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
)}
|
||||
{isWatched && onStopWatching && (
|
||||
<ContextMenuItem onClick={() => onStopWatching(folder)}>
|
||||
<EyeOff className="mr-2 h-4 w-4" />
|
||||
Stop watching
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => onMove(folder)}>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => onDelete(folder)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
)}
|
||||
<ContextMenuItem onClick={() => onCreateSubfolder(folder.id)}>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => startRename()}>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => onMove(folder)}>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => onDelete(folder)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ interface FolderTreeViewProps {
|
|||
targetFolderId: number | null
|
||||
) => void;
|
||||
onReorderFolder?: (folderId: number, beforePos: string | null, afterPos: string | null) => void;
|
||||
watchedFolderIds?: Set<number>;
|
||||
onRescanFolder?: (folder: FolderDisplay) => void;
|
||||
onStopWatchingFolder?: (folder: FolderDisplay) => void;
|
||||
}
|
||||
|
||||
function groupBy<T>(items: T[], keyFn: (item: T) => string | number): Record<string | number, T[]> {
|
||||
|
|
@ -73,6 +76,9 @@ export function FolderTreeView({
|
|||
searchQuery,
|
||||
onDropIntoFolder,
|
||||
onReorderFolder,
|
||||
watchedFolderIds,
|
||||
onRescanFolder,
|
||||
onStopWatchingFolder,
|
||||
}: FolderTreeViewProps) {
|
||||
const foldersByParent = useMemo(() => groupBy(folders, (f) => f.parentId ?? "root"), [folders]);
|
||||
|
||||
|
|
@ -204,6 +210,9 @@ export function FolderTreeView({
|
|||
siblingPositions={siblingPositions}
|
||||
contextMenuOpen={openContextMenuId === `folder-${f.id}`}
|
||||
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `folder-${f.id}` : null)}
|
||||
isWatched={watchedFolderIds?.has(f.id)}
|
||||
onRescan={onRescanFolder}
|
||||
onStopWatching={onStopWatchingFolder}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import dynamic from "next/dynamic";
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { VersionHistoryButton } from "@/components/documents/version-history";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||
|
|
@ -180,12 +181,16 @@ export function EditorPanelContent({
|
|||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-4 py-2 shrink-0 border-b">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-sm font-semibold truncate">{displayTitle}</h2>
|
||||
{isEditableType && editedMarkdown !== null && (
|
||||
<p className="text-[10px] text-muted-foreground">Unsaved changes</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-sm font-semibold truncate">{displayTitle}</h2>
|
||||
{isEditableType && editedMarkdown !== null && (
|
||||
<p className="text-[10px] text-muted-foreground">Unsaved changes</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{editorDoc?.document_type && (
|
||||
<VersionHistoryButton documentId={documentId} documentType={editorDoc.document_type} />
|
||||
)}
|
||||
{onClose && (
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">
|
||||
<XIcon className="size-4" />
|
||||
|
|
@ -193,6 +198,7 @@ export function EditorPanelContent({
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{isLoading ? (
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { foldersApiService } from "@/lib/apis/folders-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { queries } from "@/zero/queries/index";
|
||||
|
|
@ -92,6 +93,24 @@ export function DocumentsSidebar({
|
|||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebouncedValue(search, 250);
|
||||
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
||||
const [watchedFolderIds, setWatchedFolderIds] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const api = typeof window !== "undefined" ? window.electronAPI : null;
|
||||
if (!api?.getWatchedFolders) return;
|
||||
|
||||
async function loadWatchedIds() {
|
||||
const folders = await api!.getWatchedFolders();
|
||||
const ids = new Set(
|
||||
folders
|
||||
.filter((f) => f.rootFolderId != null)
|
||||
.map((f) => f.rootFolderId as number)
|
||||
);
|
||||
setWatchedFolderIds(ids);
|
||||
}
|
||||
|
||||
loadWatchedIds();
|
||||
}, []);
|
||||
const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom);
|
||||
|
||||
const [sidebarDocs, setSidebarDocs] = useAtom(sidebarSelectedDocumentsAtom);
|
||||
|
|
@ -223,6 +242,87 @@ export function DocumentsSidebar({
|
|||
[createFolderParentId, searchSpaceId, setExpandedFolderMap]
|
||||
);
|
||||
|
||||
const isElectron = typeof window !== "undefined" && !!window.electronAPI;
|
||||
|
||||
const handleWatchFolder = useCallback(async () => {
|
||||
const api = window.electronAPI;
|
||||
if (!api) return;
|
||||
|
||||
const folderPath = await api.selectFolder();
|
||||
if (!folderPath) return;
|
||||
|
||||
const folderName = folderPath.split("/").pop() || folderPath.split("\\").pop() || folderPath;
|
||||
|
||||
try {
|
||||
const result = await documentsApiService.folderIndex(searchSpaceId, {
|
||||
folder_path: folderPath,
|
||||
folder_name: folderName,
|
||||
search_space_id: searchSpaceId,
|
||||
});
|
||||
|
||||
const rootFolderId = (result as { root_folder_id?: number })?.root_folder_id ?? null;
|
||||
|
||||
await api.addWatchedFolder({
|
||||
path: folderPath,
|
||||
name: folderName,
|
||||
excludePatterns: [".git", "node_modules", "__pycache__", ".DS_Store", ".obsidian", ".trash"],
|
||||
fileExtensions: null,
|
||||
rootFolderId,
|
||||
searchSpaceId,
|
||||
active: true,
|
||||
});
|
||||
|
||||
toast.success(`Watching folder: ${folderName}`);
|
||||
} catch (err) {
|
||||
toast.error((err as Error)?.message || "Failed to watch folder");
|
||||
}
|
||||
}, [searchSpaceId]);
|
||||
|
||||
const handleRescanFolder = useCallback(
|
||||
async (folder: FolderDisplay) => {
|
||||
const api = window.electronAPI;
|
||||
if (!api) return;
|
||||
|
||||
const watchedFolders = await api.getWatchedFolders();
|
||||
const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
|
||||
if (!matched) {
|
||||
toast.error("This folder is not being watched");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await documentsApiService.folderIndex(searchSpaceId, {
|
||||
folder_path: matched.path,
|
||||
folder_name: matched.name,
|
||||
search_space_id: searchSpaceId,
|
||||
root_folder_id: folder.id,
|
||||
});
|
||||
toast.success(`Re-scanning folder: ${matched.name}`);
|
||||
} catch (err) {
|
||||
toast.error((err as Error)?.message || "Failed to re-scan folder");
|
||||
}
|
||||
},
|
||||
[searchSpaceId]
|
||||
);
|
||||
|
||||
const handleStopWatching = useCallback(
|
||||
async (folder: FolderDisplay) => {
|
||||
const api = window.electronAPI;
|
||||
if (!api) return;
|
||||
|
||||
const watchedFolders = await api.getWatchedFolders();
|
||||
const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
|
||||
if (!matched) {
|
||||
toast.error("This folder is not being watched");
|
||||
return;
|
||||
}
|
||||
|
||||
await api.removeWatchedFolder(matched.path);
|
||||
toast.success(`Stopped watching: ${matched.name}`);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => {
|
||||
try {
|
||||
await foldersApiService.updateFolder(folder.id, { name: newName });
|
||||
|
|
@ -641,14 +741,15 @@ export function DocumentsSidebar({
|
|||
|
||||
<div className="flex-1 min-h-0 overflow-x-hidden pt-0 flex flex-col">
|
||||
<div className="px-4 pb-2">
|
||||
<DocumentsFilters
|
||||
typeCounts={typeCounts}
|
||||
onSearch={setSearch}
|
||||
searchValue={search}
|
||||
onToggleType={onToggleType}
|
||||
activeTypes={activeTypes}
|
||||
onCreateFolder={() => handleCreateFolder(null)}
|
||||
/>
|
||||
<DocumentsFilters
|
||||
typeCounts={typeCounts}
|
||||
onSearch={setSearch}
|
||||
searchValue={search}
|
||||
onToggleType={onToggleType}
|
||||
activeTypes={activeTypes}
|
||||
onCreateFolder={() => handleCreateFolder(null)}
|
||||
onWatchFolder={isElectron ? handleWatchFolder : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{deletableSelectedIds.length > 0 && (
|
||||
|
|
@ -666,39 +767,42 @@ export function DocumentsSidebar({
|
|||
)}
|
||||
|
||||
<FolderTreeView
|
||||
folders={treeFolders}
|
||||
documents={searchFilteredDocuments}
|
||||
expandedIds={expandedIds}
|
||||
onToggleExpand={toggleFolderExpand}
|
||||
mentionedDocIds={mentionedDocIds}
|
||||
onToggleChatMention={handleToggleChatMention}
|
||||
onToggleFolderSelect={handleToggleFolderSelect}
|
||||
onRenameFolder={handleRenameFolder}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
onMoveFolder={handleMoveFolder}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
searchQuery={debouncedSearch.trim() || undefined}
|
||||
onPreviewDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onEditDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||
onMoveDocument={handleMoveDocument}
|
||||
onExportDocument={handleExportDocument}
|
||||
activeTypes={activeTypes}
|
||||
onDropIntoFolder={handleDropIntoFolder}
|
||||
onReorderFolder={handleReorderFolder}
|
||||
/>
|
||||
folders={treeFolders}
|
||||
documents={searchFilteredDocuments}
|
||||
expandedIds={expandedIds}
|
||||
onToggleExpand={toggleFolderExpand}
|
||||
mentionedDocIds={mentionedDocIds}
|
||||
onToggleChatMention={handleToggleChatMention}
|
||||
onToggleFolderSelect={handleToggleFolderSelect}
|
||||
onRenameFolder={handleRenameFolder}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
onMoveFolder={handleMoveFolder}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
searchQuery={debouncedSearch.trim() || undefined}
|
||||
onPreviewDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onEditDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||
onMoveDocument={handleMoveDocument}
|
||||
onExportDocument={handleExportDocument}
|
||||
activeTypes={activeTypes}
|
||||
onDropIntoFolder={handleDropIntoFolder}
|
||||
onReorderFolder={handleReorderFolder}
|
||||
watchedFolderIds={watchedFolderIds}
|
||||
onRescanFolder={handleRescanFolder}
|
||||
onStopWatchingFolder={handleStopWatching}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FolderPickerDialog
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue