From df65620d85eacc3c47427840ea91f9be8fc3b3cd Mon Sep 17 00:00:00 2001 From: Reece Date: Fri, 6 Mar 2026 18:40:27 +0000 Subject: [PATCH] Watch folders set up --- .../public/locales/en-GB/translation.toml | 30 +- frontend/src/core/App.tsx | 2 + frontend/src/core/components/AppProviders.tsx | 5 +- .../components/fileManager/FileListItem.tsx | 34 ++ .../core/components/shared/QuickAccessBar.tsx | 27 +- .../components/smartFolders/IconPicker.tsx | 7 + .../smartFolders/SmartFolderCard.tsx | 90 +++++ .../smartFolders/SmartFolderHomePage.tsx | 367 ++++++++++++++++++ .../SmartFolderManagementModal.tsx | 10 +- .../smartFolders/SmartFolderSection.tsx | 121 ++---- .../smartFolders/SmartFolderSidebarPanel.tsx | 69 ++++ .../smartFolders/SmartFolderWorkbenchView.tsx | 26 +- .../smartFolders/SmartFoldersRegistration.tsx | 38 ++ .../src/core/components/tools/ToolPanel.tsx | 164 ++++---- .../src/core/contexts/FolderFileContext.tsx | 59 +++ frontend/src/core/hooks/useAllSmartFolders.ts | 29 ++ .../src/core/hooks/useFolderMembership.ts | 42 ++ .../src/core/hooks/useSmartFolderSidebar.ts | 8 + .../core/services/folderRunStateStorage.ts | 2 +- frontend/src/core/services/folderStorage.ts | 2 +- .../src/core/services/smartFolderStorage.ts | 2 +- frontend/src/core/styles/theme.css | 4 + frontend/src/proprietary/App.tsx | 3 +- 23 files changed, 951 insertions(+), 190 deletions(-) create mode 100644 frontend/src/core/components/smartFolders/IconPicker.tsx create mode 100644 frontend/src/core/components/smartFolders/SmartFolderCard.tsx create mode 100644 frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx create mode 100644 frontend/src/core/components/smartFolders/SmartFolderSidebarPanel.tsx create mode 100644 frontend/src/core/components/smartFolders/SmartFoldersRegistration.tsx create mode 100644 frontend/src/core/contexts/FolderFileContext.tsx create mode 100644 frontend/src/core/hooks/useAllSmartFolders.ts create mode 100644 frontend/src/core/hooks/useFolderMembership.ts create mode 100644 frontend/src/core/hooks/useSmartFolderSidebar.ts diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 11336220c5..d17e376451 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -2972,6 +2972,7 @@ addFiles = "Add Files" [fileManager] active = "Active" +addToSmartFolder = "Add to Watch Folder" addToUpload = "Add to Upload" clearAll = "Clear All" clearSelection = "Clear Selection" @@ -4926,6 +4927,7 @@ help = "Help" read = "Read" reader = "Reader" settings = "Settings" +watchFolders = "Watch Folders" showMeAround = "Show me around" sign = "Sign" tours = "Tours" @@ -6972,8 +6974,8 @@ deleteConfirmTitle = "Delete folder?" defaultFolderWarning = "This is a default folder and will be recreated on next reload." folderNotFound = "Folder not found" newFolder = "New folder" -noFolders = "No smart folders yet" -title = "Smart Folders" +noFolders = "No watch folders yet" +title = "Watch Folders" [smartFolders.modal] automation = "Automation" @@ -6981,17 +6983,31 @@ automationSaved = "Automation saved" createFolder = "Create Folder" stepsSaved = "Steps saved — click Create Folder to finish" color = "Accent color" -createTitle = "New Smart Folder" +createTitle = "New Watch Folder" description = "Description" descriptionPlaceholder = "What does this folder do?" -editTitle = "Edit Smart Folder" +editTitle = "Edit Watch Folder" icon = "Icon" name = "Folder name" -namePlaceholder = "My Smart Folder" +namePlaceholder = "My Watch Folder" nameRequired = "Folder name is required" nameTooLong = "Folder name must be 50 characters or less" saveChanges = "Save Changes" +[smartFolders.home] +create = "Create your first folder" +dropHere = "Drop to process" +editFolder = "Edit folder" +empty = "No watch folders yet" +file = "file" +files = "files" +openFolder = "Open folder" +title = "Watch Folders" + +[smartFolders.status] +done = "Done" +processing = "Processing" + [smartFolders.workbench] all = "All" download = "Download" @@ -7007,6 +7023,10 @@ processing = "Processing" processingLog = "Processing log" step = "Step {{step}}: {{operation}}" +sidebarFiles = "Your Files" +sidebarOutputFiles = "Output Files" +sidebarSelectFolder = "Folder Files" + [zipWarning] cancel = "Cancel" confirm = "Extract" diff --git a/frontend/src/core/App.tsx b/frontend/src/core/App.tsx index f53031c4ba..9f1a6bbeb4 100644 --- a/frontend/src/core/App.tsx +++ b/frontend/src/core/App.tsx @@ -8,6 +8,7 @@ import { PreferencesProvider } from "@app/contexts/PreferencesContext"; import HomePage from "@app/pages/HomePage"; import MobileScannerPage from "@app/pages/MobileScannerPage"; import Onboarding from "@app/components/onboarding/Onboarding"; +import SmartFoldersRegistration from "@app/components/smartFolders/SmartFoldersRegistration"; // Import global styles import "@app/styles/tailwind.css"; @@ -50,6 +51,7 @@ export default function App() { + } diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 75c7d281c3..660f7a3d43 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -24,6 +24,7 @@ import { useLogoAssets } from '@app/hooks/useLogoAssets'; import AppConfigLoader from '@app/components/shared/AppConfigLoader'; import { RedactionProvider } from "@app/contexts/RedactionContext"; import { FormFillProvider } from "@app/tools/formFill/FormFillContext"; +import { FolderFileContextProvider } from "@app/contexts/FolderFileContext"; // Component to initialize scarf tracking (must be inside AppConfigProvider) function ScarfTrackingInitializer() { @@ -123,7 +124,9 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide - {children} + + {children} + diff --git a/frontend/src/core/components/fileManager/FileListItem.tsx b/frontend/src/core/components/fileManager/FileListItem.tsx index bab58c1b21..0e0e6f1d09 100644 --- a/frontend/src/core/components/fileManager/FileListItem.tsx +++ b/frontend/src/core/components/fileManager/FileListItem.tsx @@ -16,6 +16,10 @@ import ToolChain from '@app/components/shared/ToolChain'; import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; import { PrivateContent } from '@app/components/shared/PrivateContent'; import { useFileManagement } from '@app/contexts/FileContext'; +import { useAllSmartFolders } from '@app/hooks/useAllSmartFolders'; +import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; +import { useNavigationActions } from '@app/contexts/NavigationContext'; +import { iconMap } from '@app/components/tools/automate/iconMap'; interface FileListItemProps { file: StirlingFileStub; @@ -48,6 +52,12 @@ const FileListItem: React.FC = ({ const { t } = useTranslation(); const { expandedFileIds, onToggleExpansion, onUnzipFile } = useFileManagerContext(); const { removeFiles } = useFileManagement(); + const smartFolders = useAllSmartFolders(); + const { setCustomWorkbenchViewData } = useToolWorkflow(); + const { actions } = useNavigationActions(); + + const isPdf = file.name?.toLowerCase().endsWith('.pdf') ?? false; + const showSmartFolders = isPdf && smartFolders.length > 0; // Check if this is a ZIP file const isZipFile = zipFileService.isZipFileStub(file); @@ -260,6 +270,30 @@ const FileListItem: React.FC = ({ )} + {showSmartFolders && ( + <> + + {t('fileManager.addToSmartFolder', 'Add to Watch Folder')} + {smartFolders.map((folder) => { + const FolderItemIcon = iconMap[folder.icon as keyof typeof iconMap] || iconMap.FolderIcon; + return ( + } + onClick={(e) => { + e.stopPropagation(); + setCustomWorkbenchViewData('smartFolder', { folderId: folder.id, pendingFileId: file.id }); + actions.setWorkbench('custom:smartFolder'); + }} + > + {folder.name} + + ); + })} + + + )} + } onClick={(e) => { diff --git a/frontend/src/core/components/shared/QuickAccessBar.tsx b/frontend/src/core/components/shared/QuickAccessBar.tsx index cac8704f79..a472c5ec56 100644 --- a/frontend/src/core/components/shared/QuickAccessBar.tsx +++ b/frontend/src/core/components/shared/QuickAccessBar.tsx @@ -7,6 +7,7 @@ import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvi import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext'; +import { SMART_FOLDER_VIEW_ID, SMART_FOLDER_WORKBENCH_ID } from '@app/components/smartFolders/SmartFoldersRegistration'; import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation'; import { handleUnlessSpecialClick } from '@app/utils/clickHandlers'; import { ButtonConfig } from '@app/types/sidebar'; @@ -15,6 +16,7 @@ import { Tooltip } from '@app/components/shared/Tooltip'; import AllToolsNavButton from '@app/components/shared/AllToolsNavButton'; import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolButton"; import AppConfigModal from '@app/components/shared/AppConfigModal'; +import FolderSpecialRoundedIcon from '@mui/icons-material/FolderSpecialRounded'; import { useAppConfig } from '@app/contexts/AppConfigContext'; import { useLicenseAlert } from "@app/hooks/useLicenseAlert"; import { requestStartTour } from '@app/constants/events'; @@ -34,8 +36,8 @@ const QuickAccessBar = forwardRef((_, ref) => { const location = useLocation(); const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); - const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool, toolAvailability } = useToolWorkflow(); - const { hasUnsavedChanges } = useNavigationState(); + const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool, toolAvailability, setCustomWorkbenchViewData } = useToolWorkflow(); + const { hasUnsavedChanges, workbench } = useNavigationState(); const { actions: navigationActions } = useNavigationActions(); const { getToolNavigation } = useSidebarNavigation(); const { config } = useAppConfig(); @@ -61,9 +63,13 @@ const QuickAccessBar = forwardRef((_, ref) => { }, [location.pathname]); useEffect(() => { + if (workbench === SMART_FOLDER_WORKBENCH_ID) { + setActiveButton('watchFolders'); + return; + } const next = getActiveNavButton(selectedToolKey, readerMode); setActiveButton(next); - }, [leftPanelView, selectedToolKey, toolRegistry, readerMode]); + }, [leftPanelView, selectedToolKey, toolRegistry, readerMode, workbench]); const handleFilesButtonClick = () => { openFilesModal(); @@ -158,6 +164,8 @@ const QuickAccessBar = forwardRef((_, ref) => { return availability?.available !== false; }), [t, setActiveButton, handleReaderToggle, selectedToolKey, resetTool, handleToolSelect, toolAvailability]); + const isWatchFoldersActive = workbench === SMART_FOLDER_WORKBENCH_ID; + const middleButtons: ButtonConfig[] = [ { id: 'files', @@ -168,6 +176,19 @@ const QuickAccessBar = forwardRef((_, ref) => { type: 'modal', onClick: handleFilesButtonClick }, + { + id: 'watchFolders', + name: t("quickAccess.watchFolders", "Watch Folders"), + icon: , + isRound: true, + size: 'md', + type: 'navigation', + onClick: () => { + setActiveButton('watchFolders'); + setCustomWorkbenchViewData(SMART_FOLDER_VIEW_ID, { folderId: null }); + navigationActions.setWorkbench(SMART_FOLDER_WORKBENCH_ID); + } + }, ]; //TODO: Activity //{ diff --git a/frontend/src/core/components/smartFolders/IconPicker.tsx b/frontend/src/core/components/smartFolders/IconPicker.tsx new file mode 100644 index 0000000000..683e8ebf22 --- /dev/null +++ b/frontend/src/core/components/smartFolders/IconPicker.tsx @@ -0,0 +1,7 @@ +/** + * Icon picker for Smart Folder create/edit. + * Re-exports IconSelector from the automate tools so smart folder components + * don't need to reach into the automate tools directory. + */ + +export { default as IconPicker } from '@app/components/tools/automate/IconSelector'; diff --git a/frontend/src/core/components/smartFolders/SmartFolderCard.tsx b/frontend/src/core/components/smartFolders/SmartFolderCard.tsx new file mode 100644 index 0000000000..4d05df73a9 --- /dev/null +++ b/frontend/src/core/components/smartFolders/SmartFolderCard.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { Box, Button, Text, ActionIcon, Group, Loader } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import { SmartFolder } from '@app/types/smartFolders'; +import { FolderRunStatus } from '@app/hooks/useFolderRunStatuses'; +import { iconMap } from '@app/components/tools/automate/iconMap'; + +interface SmartFolderCardProps { + folder: SmartFolder; + isActive: boolean; + status: FolderRunStatus; + onSelect: () => void; + onEdit: (e: React.MouseEvent) => void; + onDelete: (e: React.MouseEvent) => void; +} + +export function SmartFolderCard({ folder, isActive, status, onSelect, onEdit, onDelete }: SmartFolderCardProps) { + const { t } = useTranslation(); + const [isHovered, setIsHovered] = useState(false); + const IconComponent = iconMap[folder.icon as keyof typeof iconMap] || iconMap.FolderIcon; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + ); +} diff --git a/frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx b/frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx new file mode 100644 index 0000000000..77c253c77e --- /dev/null +++ b/frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx @@ -0,0 +1,367 @@ +import { useState, useCallback, useEffect } from 'react'; +import { Box, Text, Stack, Group, ActionIcon, Badge, Button, Loader, ScrollArea } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import AddIcon from '@mui/icons-material/Add'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import EditIcon from '@mui/icons-material/Edit'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import { useSmartFolders } from '@app/hooks/useSmartFolders'; +import { useFolderRunStatuses } from '@app/hooks/useFolderRunStatuses'; +import { SmartFolder, SmartFolderRunEntry } from '@app/types/smartFolders'; +import { AutomationConfig } from '@app/types/automation'; +import { iconMap } from '@app/components/tools/automate/iconMap'; +import { automationStorage } from '@app/services/automationStorage'; +import { folderStorage } from '@app/services/folderStorage'; +import { folderRunStateStorage } from '@app/services/folderRunStateStorage'; +import { executeAutomationSequence } from '@app/utils/automationExecutor'; +import { SmartFolderManagementModal } from '@app/components/smartFolders/SmartFolderManagementModal'; +import { DeleteFolderConfirmModal } from '@app/components/smartFolders/DeleteFolderConfirmModal'; +import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; +import { useNavigationActions } from '@app/contexts/NavigationContext'; +import { SMART_FOLDER_VIEW_ID, SMART_FOLDER_WORKBENCH_ID } from '@app/components/smartFolders/SmartFoldersRegistration'; + +// Humanise an operation key like "compress-pdf" → "Compress PDF" +function humaniseOp(op: string): string { + return op + .replace(/[-_]/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()); +} + +interface FolderCardProps { + folder: SmartFolder; + status: 'idle' | 'processing' | 'done'; + isProcessing: boolean; + onEdit: (folder: SmartFolder) => void; + onOpen: (folderId: string) => void; + onDropFiles: (folder: SmartFolder, files: File[]) => void; +} + +function FolderCard({ folder, status, isProcessing, onEdit, onOpen, onDropFiles }: FolderCardProps) { + const { t } = useTranslation(); + const [automation, setAutomation] = useState(null); + const [fileCount, setFileCount] = useState(0); + const [isDragOver, setIsDragOver] = useState(false); + + useEffect(() => { + automationStorage.getAutomation(folder.automationId).then(setAutomation); + + const loadCount = () => + folderStorage.getFolderData(folder.id).then((record) => { + setFileCount(record ? Object.keys(record.files).length : 0); + }); + loadCount(); + + const unsub = folderStorage.onFolderChange((changedId) => { + if (changedId === folder.id) loadCount(); + }); + return unsub; + }, [folder.id, folder.automationId]); + + const FolderIcon = iconMap[folder.icon as keyof typeof iconMap] ?? iconMap.FolderIcon; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(true); + }; + const handleDragLeave = (e: React.DragEvent) => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsDragOver(false); + }; + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + onDropFiles(folder, Array.from(e.dataTransfer.files)); + }; + + return ( + + + {/* Icon circle */} + + + + + {/* Main content */} + + {/* Top row: name + badges + actions */} + + + + {folder.name} + + {fileCount > 0 && ( + + {fileCount} {fileCount === 1 + ? t('smartFolders.home.file', 'file') + : t('smartFolders.home.files', 'files')} + + )} + {isProcessing && ( + + {t('smartFolders.status.processing', 'Processing')} + + )} + {status === 'done' && !isProcessing && ( + + {t('smartFolders.status.done', 'Done')} + + )} + + + + { e.stopPropagation(); onEdit(folder); }} + aria-label={t('smartFolders.home.editFolder', 'Edit folder')} + > + + + { e.stopPropagation(); onOpen(folder.id); }} + aria-label={t('smartFolders.home.openFolder', 'Open folder')} + > + + + + + + {/* Description */} + {folder.description && ( + + {folder.description} + + )} + + {/* Operation pills */} + {automation && automation.operations.length > 0 && ( + + {automation.operations.map((op, i) => ( + + {humaniseOp(op.operation)} + + ))} + + )} + + + + {/* Drop overlay */} + {isDragOver && ( + + + + + {t('smartFolders.home.dropHere', 'Drop to process')} + + + + )} + + ); +} + +export function SmartFolderHomePage() { + const { t } = useTranslation(); + const { folders, loading, deleteFolder, refreshFolders } = useSmartFolders(); + const statuses = useFolderRunStatuses(folders); + const { toolRegistry, setCustomWorkbenchViewData } = useToolWorkflow(); + const { actions } = useNavigationActions(); + + const [createModalOpen, setCreateModalOpen] = useState(false); + const [editFolder, setEditFolder] = useState(null); + const [editAutomation, setEditAutomation] = useState(null); + const [processingFolderIds, setProcessingFolderIds] = useState>(new Set()); + const [deleteTarget, setDeleteTarget] = useState(null); + + const navigateToFolder = useCallback((folderId: string) => { + setCustomWorkbenchViewData(SMART_FOLDER_VIEW_ID, { folderId }); + actions.setWorkbench(SMART_FOLDER_WORKBENCH_ID); + }, [setCustomWorkbenchViewData, actions]); + + const handleEdit = useCallback(async (folder: SmartFolder) => { + setEditFolder(folder); + const automation = await automationStorage.getAutomation(folder.automationId); + setEditAutomation(automation); + setCreateModalOpen(true); + }, []); + + const handleModalClose = () => { + setCreateModalOpen(false); + setEditFolder(null); + setEditAutomation(null); + }; + + const processFiles = useCallback(async (folder: SmartFolder, files: File[]) => { + const pdfs = files.filter(f => f.name.toLowerCase().endsWith('.pdf')); + if (pdfs.length === 0) return; + + setProcessingFolderIds(prev => new Set([...prev, folder.id])); + try { + const automation: AutomationConfig | null = await automationStorage.getAutomation(folder.automationId); + if (!automation) return; + + for (const file of pdfs) { + const inputFileId = `input-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + await folderStorage.addFileToFolder(folder.id, inputFileId, { status: 'processing' }); + try { + const resultFiles = await executeAutomationSequence(automation, [file], toolRegistry); + const existingRuns = await folderRunStateStorage.getFolderRunState(folder.id); + const newRuns: SmartFolderRunEntry[] = [...existingRuns]; + for (const resultFile of resultFiles) { + const outputId = `output-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + await folderStorage.storeOutputFile(folder.id, outputId, resultFile, resultFile.name); + newRuns.push({ inputFileId, displayFileId: outputId, status: 'processed' }); + } + await folderStorage.updateFileMetadata(folder.id, inputFileId, { + status: 'processed', + processedAt: new Date(), + }); + await folderRunStateStorage.setFolderRunState(folder.id, newRuns); + } catch { + await folderStorage.updateFileMetadata(folder.id, inputFileId, { status: 'error' }); + } + } + } finally { + setProcessingFolderIds(prev => { + const next = new Set(prev); + next.delete(folder.id); + return next; + }); + } + }, [toolRegistry]); + + const handleDeleteConfirm = async () => { + if (!deleteTarget) return; + await deleteFolder(deleteTarget.id); + setDeleteTarget(null); + }; + + return ( + + {/* Header */} + + + {t('smartFolders.home.title', 'Watch Folders')} + + + + + {/* Folder list */} + + + {loading ? ( + + + + ) : folders.length === 0 ? ( + + + {t('smartFolders.home.empty', 'No watch folders yet')} + + + + ) : ( + + {folders.map((folder) => { + const status = statuses[folder.id] ?? 'idle'; + const isProcessing = processingFolderIds.has(folder.id) || status === 'processing'; + return ( + + ); + })} + + )} + + + + + setDeleteTarget(null)} + /> + + ); +} diff --git a/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx b/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx index 6f68d09fae..7c5dec5501 100644 --- a/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx +++ b/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx @@ -15,7 +15,7 @@ import { import { useTranslation } from 'react-i18next'; import { SmartFolder } from '@app/types/smartFolders'; import { AutomationConfig, AutomationMode } from '@app/types/automation'; -import IconSelector from '@app/components/tools/automate/IconSelector'; +import { IconPicker as IconSelector } from '@app/components/smartFolders/IconPicker'; import AutomationCreation from '@app/components/tools/automate/AutomationCreation'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { smartFolderStorage } from '@app/services/smartFolderStorage'; @@ -119,8 +119,8 @@ export function SmartFolderManagementModal({ }; const title = isEditMode - ? t('smartFolders.modal.editTitle', 'Edit Smart Folder') - : t('smartFolders.modal.createTitle', 'New Smart Folder'); + ? t('smartFolders.modal.editTitle', 'Edit Watch Folder') + : t('smartFolders.modal.createTitle', 'New Watch Folder'); return ( { setName(e.currentTarget.value); setNameError(''); }} error={nameError} @@ -197,7 +197,7 @@ export function SmartFolderManagementModal({ onComplete={handleAutomationComplete} toolRegistry={toolRegistry} hideMetadata - nameOverride={name.trim() || 'Smart Folder Automation'} + nameOverride={name.trim() || 'Watch Folder Automation'} saveTriggerRef={automationSaveTrigger} /> diff --git a/frontend/src/core/components/smartFolders/SmartFolderSection.tsx b/frontend/src/core/components/smartFolders/SmartFolderSection.tsx index 110a524797..e767f4b501 100644 --- a/frontend/src/core/components/smartFolders/SmartFolderSection.tsx +++ b/frontend/src/core/components/smartFolders/SmartFolderSection.tsx @@ -1,25 +1,18 @@ -import { useState, useEffect } from 'react'; -import { Box, Button, Text, Stack, ActionIcon, Group, Loader } from '@mantine/core'; +import { useState } from 'react'; +import { Box, Button, Text, Stack } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import AddIcon from '@mui/icons-material/Add'; -import EditIcon from '@mui/icons-material/Edit'; -import DeleteIcon from '@mui/icons-material/Delete'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import { useSmartFolders } from '@app/hooks/useSmartFolders'; import { useFolderRunStatuses } from '@app/hooks/useFolderRunStatuses'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useNavigationActions } from '@app/contexts/NavigationContext'; import { SmartFolderManagementModal } from '@app/components/smartFolders/SmartFolderManagementModal'; import { DeleteFolderConfirmModal } from '@app/components/smartFolders/DeleteFolderConfirmModal'; -import { SmartFolderWorkbenchView } from '@app/components/smartFolders/SmartFolderWorkbenchView'; +import { SmartFolderCard } from '@app/components/smartFolders/SmartFolderCard'; import { SmartFolder } from '@app/types/smartFolders'; import { AutomationConfig } from '@app/types/automation'; import { automationStorage } from '@app/services/automationStorage'; -import { seedDefaultFolders } from '@app/data/smartFolderPresets'; -import { iconMap } from '@app/components/tools/automate/iconMap'; - -const SMART_FOLDER_VIEW_ID = 'smartFolder'; -const SMART_FOLDER_WORKBENCH_ID = 'custom:smartFolder' as const; +import { SMART_FOLDER_VIEW_ID, SMART_FOLDER_WORKBENCH_ID } from '@app/components/smartFolders/SmartFoldersRegistration'; export function SmartFolderSection() { const { t } = useTranslation(); @@ -28,28 +21,13 @@ export function SmartFolderSection() { const [editAutomation, setEditAutomation] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [activeFolderId, setActiveFolderId] = useState(null); - const [hoveredId, setHoveredId] = useState(null); const { folders, loading, deleteFolder, refreshFolders } = useSmartFolders(); const statuses = useFolderRunStatuses(folders); - const { registerCustomWorkbenchView, unregisterCustomWorkbenchView, setCustomWorkbenchViewData } = useToolWorkflow(); + const { setCustomWorkbenchViewData } = useToolWorkflow(); const { actions } = useNavigationActions(); - useEffect(() => { - seedDefaultFolders(); - }, []); - - useEffect(() => { - registerCustomWorkbenchView({ - id: SMART_FOLDER_VIEW_ID, - workbenchId: SMART_FOLDER_WORKBENCH_ID, - label: t('smartFolders.title', 'Smart Folders'), - component: SmartFolderWorkbenchView, - }); - return () => unregisterCustomWorkbenchView(SMART_FOLDER_VIEW_ID); - }, [registerCustomWorkbenchView, unregisterCustomWorkbenchView, t]); - const handleFolderClick = (folderId: string) => { setActiveFolderId(folderId); setCustomWorkbenchViewData(SMART_FOLDER_VIEW_ID, { folderId }); @@ -96,89 +74,42 @@ export function SmartFolderSection() { overflow: 'hidden', }} > - {/* Section header — matches .tool-subcategory-row style */} - - {t('smartFolders.title', 'Smart Folders')} + { + setCustomWorkbenchViewData(SMART_FOLDER_VIEW_ID, { folderId: null }); + actions.setWorkbench(SMART_FOLDER_WORKBENCH_ID); + }} + > + {t('smartFolders.title', 'Watch Folders')} - {/* Scrollable folder list */} {!loading && folders.length === 0 && ( - {t('smartFolders.noFolders', 'No smart folders yet')} + {t('smartFolders.noFolders', 'No watch folders yet')} )} - {folders.map((folder) => { - const IconComponent = iconMap[folder.icon as keyof typeof iconMap] || iconMap.FolderIcon; - const status = statuses[folder.id] ?? 'idle'; - const isActive = activeFolderId === folder.id; - const isHovered = hoveredId === folder.id; + {folders.map((folder) => ( + handleFolderClick(folder.id)} + onEdit={(e) => handleEditFolder(e, folder)} + onDelete={(e) => handleDeleteClick(e, folder)} + /> + ))} - return ( - setHoveredId(folder.id)} - onMouseLeave={() => setHoveredId(null)} - > - - - ); - })} - - {/* New folder button */}