diff --git a/eslint.config.js b/eslint.config.js index 092408a..10a64ff 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,17 @@ export default tseslint.config( 'warn', { allowConstantExport: true }, ], + 'no-restricted-imports': [ + 'warn', + { + patterns: [ + { + group: ['**/components/FolderTree/FolderTree*', '**/FolderTree/FolderTree*'], + message: 'FolderTree is deprecated β€” use FormGeneratorTable with groupingConfig instead.', + }, + ], + }, + ], }, }, ) diff --git a/src/api/fileApi.ts b/src/api/fileApi.ts index 44102f1..e251006 100644 --- a/src/api/fileApi.ts +++ b/src/api/fileApi.ts @@ -190,110 +190,87 @@ export async function deleteFiles( return uniqueIds.map(fileId => ({ success: true, fileId })); } -export async function deleteFolders( - request: ApiRequestFunction, - folderIds: string[], - recursiveFolders: boolean = true -): Promise<{ deletedFiles: number; deletedFolders: number }> { - const uniqueIds = [...new Set(folderIds.filter(Boolean))]; - if (uniqueIds.length === 0) return { deletedFiles: 0, deletedFolders: 0 }; - return await request({ - url: '/api/files/batch-delete', - method: 'post', - data: { folderIds: uniqueIds, recursiveFolders } - }); -} - // ============================================================================ -// FOLDER API FUNCTIONS +// GROUP BULK API FUNCTIONS // ============================================================================ -export interface FolderInfo { - id: string; - name: string; - parentId: string | null; - fileCount?: number; - mandateId?: string; - featureInstanceId?: string; - createdAt?: number; - scope?: string; - neutralize?: boolean; -} - -export async function fetchFolders( +/** Patch scope for all files in a group (recursive) */ +export async function patchGroupScope( request: ApiRequestFunction, - parentId?: string | null -): Promise { - const params: any = {}; - if (parentId !== undefined && parentId !== null) { - params.parentId = parentId; - } - const data = await request({ - url: '/api/files/folders', - method: 'get', - params, - }); - return Array.isArray(data) ? data : []; -} - -export async function createFolder( - request: ApiRequestFunction, - name: string, - parentId?: string | null -): Promise { - return await request({ - url: '/api/files/folders', - method: 'post', - data: { name, parentId: parentId || null }, - }); -} - -export async function renameFolder( - request: ApiRequestFunction, - folderId: string, - name: string + groupId: string, + scope: string ): Promise { return await request({ - url: `/api/files/folders/${folderId}`, - method: 'put', - data: { name }, + url: `/api/files/groups/${groupId}/scope`, + method: 'patch', + data: { scope }, }); } -export async function deleteFolderApi( +/** Patch neutralize for all files in a group (recursive, incl. knowledge purge/reindex) */ +export async function patchGroupNeutralize( request: ApiRequestFunction, - folderId: string, - recursive: boolean = false + groupId: string, + neutralize: boolean ): Promise { return await request({ - url: `/api/files/folders/${folderId}`, + url: `/api/files/groups/${groupId}/neutralize`, + method: 'patch', + data: { neutralize }, + }); +} + +/** Download all files in a group as ZIP */ +export async function downloadGroupZip(groupId: string): Promise { + const { default: api } = await import('../api'); + const response = await api.get(`/api/files/groups/${groupId}/download`, { + responseType: 'blob', + }); + const url = window.URL.createObjectURL(response.data); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `group-${groupId}.zip`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); +} + +/** Delete a group and optionally all its files */ +export async function deleteGroup( + request: ApiRequestFunction, + groupId: string, + deleteItems: boolean = false +): Promise { + return await request({ + url: `/api/files/groups/${groupId}`, method: 'delete', - params: { recursive }, + params: { deleteItems }, }); } -export async function moveFolder( - request: ApiRequestFunction, - folderId: string, - targetParentId: string | null -): Promise { - return await request({ - url: `/api/files/folders/${folderId}/move`, - method: 'post', - data: { targetParentId }, - }); -} - -export async function moveFile( - request: ApiRequestFunction, - fileId: string, - targetFolderId: string | null -): Promise { - return await request({ - url: `/api/files/${fileId}/move`, - method: 'post', - data: { targetFolderId }, - }); +/** Collect all file IDs belonging to a group recursively (client-side, from known groupTree) */ +export function collectGroupItemIds( + groupTree: Array<{ id: string; itemIds: string[]; subGroups: any[] }>, + groupId: string +): string[] { + const collect = (nodes: Array<{ id: string; itemIds: string[]; subGroups: any[] }>): string[] | null => { + for (const node of nodes) { + if (node.id === groupId) { + const ids: string[] = [...node.itemIds]; + const sub = (n: { id: string; itemIds: string[]; subGroups: any[] }) => { + ids.push(...n.itemIds); + n.subGroups.forEach(sub); + }; + node.subGroups.forEach(sub); + return ids; + } + const found = collect(node.subGroups); + if (found) return found; + } + return null; + }; + return collect(groupTree) ?? []; } // Note: The following operations require special handling (FormData, blob responses) diff --git a/src/components/FlowEditor/editor/EditorChatPanel.tsx b/src/components/FlowEditor/editor/EditorChatPanel.tsx index 84064ff..3ce248e 100644 --- a/src/components/FlowEditor/editor/EditorChatPanel.tsx +++ b/src/components/FlowEditor/editor/EditorChatPanel.tsx @@ -4,7 +4,7 @@ * AI Chat sidebar for the GraphicalEditor. * Streams responses via SSE (same pattern as Workspace chat). * File & data-source attachment UX mirrors WorkspaceInput: - * - Files: drag & drop from FolderTree onto input area, or click in UDB + * - Files: drag & drop from FilesTab (UDB) onto input area, or click in UDB * - Data Sources: πŸ”— picker button next to input (toggle-select from active sources) */ import React, { useState, useCallback, useEffect, useRef } from 'react'; @@ -32,7 +32,7 @@ import { useLanguage } from '../../../providers/language/LanguageContext'; export interface PendingFile { fileId: string; fileName: string; - itemType?: 'file' | 'folder'; + itemType?: 'file' | 'group'; } export interface EditorDataSource { @@ -241,7 +241,12 @@ export const EditorChatPanel: React.FC = ({ instanceId, }, [_handleSend]); const _handleDragOver = useCallback((e: React.DragEvent) => { - if (e.dataTransfer.types.includes('application/tree-items')) { + if ( + e.dataTransfer.types.includes('application/tree-items') || + e.dataTransfer.types.includes('application/group-id') || + e.dataTransfer.types.includes('application/file-id') || + e.dataTransfer.types.includes('application/file-ids') + ) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; setTreeDropOver(true); @@ -252,6 +257,12 @@ export const EditorChatPanel: React.FC = ({ instanceId, const _handleDrop = useCallback((e: React.DragEvent) => { setTreeDropOver(false); + const groupId = e.dataTransfer.getData('application/group-id'); + if (groupId) { + e.preventDefault(); + e.stopPropagation(); + return; + } const treeItemsJson = e.dataTransfer.getData('application/tree-items'); if (treeItemsJson) { e.preventDefault(); @@ -282,11 +293,11 @@ export const EditorChatPanel: React.FC = ({ instanceId, - {pf.itemType === 'folder' ? '\uD83D\uDCC1' : '\uD83D\uDCCE'} {pf.fileName.length > 20 ? pf.fileName.slice(0, 20) + '...' : pf.fileName} + {pf.itemType === 'group' ? '\uD83D\uDCC2' : '\uD83D\uDCCE'} {pf.fileName.length > 20 ? pf.fileName.slice(0, 20) + '...' : pf.fileName} {onRemovePendingFile && (