diff --git a/src/components/FlowEditor/editor/CanvasHeader.tsx b/src/components/FlowEditor/editor/CanvasHeader.tsx index d4e6de5..4caf8ec 100644 --- a/src/components/FlowEditor/editor/CanvasHeader.tsx +++ b/src/components/FlowEditor/editor/CanvasHeader.tsx @@ -23,7 +23,6 @@ import { HiOutlineArrowUturnRight, HiOutlineTrash, HiOutlineDocumentDuplicate, - HiOutlineArrowLongRight, HiOutlineChatBubbleLeftEllipsis, HiOutlineSquares2X2, } from 'react-icons/hi2'; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx index d383c22..a2f3071 100644 --- a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -754,7 +754,10 @@ const FieldBuilderEditor: React.FC = ({ param, value, onChan onChange={(e) => { const typeId = e.target.value; const nextFields = [...(Array.isArray(f.fields) ? f.fields : [])]; - const subRow = { ...(nextFields[j] as Record), type: typeId }; + const subRow: Record = { + ...(nextFields[j] as Record), + type: typeId, + }; if (formFieldTypeHasConfigurableOptions(typeId)) { subRow.options = normalizeFormFieldOptions(subRow.options); } diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx index ec55009..eab6c57 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx @@ -33,19 +33,6 @@ const _SCOPE_EMOJIS: Record = { const _NEUTRALIZE_ON_EMOJI = '\uD83D\uDD12'; // closed padlock const _NEUTRALIZE_OFF_EMOJI = '\uD83D\uDD13'; // open padlock -const _RAG_ON_EMOJI = '\uD83E\uDDE0'; // brain -const _RAG_OFF_EMOJI = '\uD83E\uDDE0'; // brain (greyed via CSS filter when off) - -/** CSS for the OFF-state of a boolean flag button. We desaturate the colour - * emoji and dim it so the on/off transition is obvious at a glance, even - * when the on/off glyph itself is similar (e.g. brain vs greyed-brain). */ -const _OFF_STATE_STYLE: React.CSSProperties = { - filter: 'grayscale(1)', - opacity: 0.45, -}; - -/** Uniform symbol for any flag whose effective value is 'mixed' across children. */ -const _MIXED_SYMBOL = '\u25E9'; /** Internal action keys reserved by the tree for the built-in flag buttons. */ const _ACTION_SCOPE = '__scope__'; @@ -209,8 +196,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ isDragging, ownership, compact, - selectable, - pendingActions, provider, onToggleExpand, onToggleSelect, @@ -223,9 +208,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ onSendToChat, onCycleScope, onToggleNeutralize, - onToggleRagIndex, - onCreateChild, - onExtraAction, onDragStart, onDragOver, onDragLeave, @@ -294,12 +276,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ const canDelete = isOwn && provider.canDelete?.(node); const canPatchScope = isOwn && provider.canPatchScope?.(node); const canPatchNeutralize = isOwn && provider.canPatchNeutralize?.(node); - const canPatchRagIndex = isOwn && provider.canPatchRagIndex?.(node); - const canCreateChild = - isOwn && - !!provider.createChild && - node.type === 'folder' && - (provider.canCreate ? provider.canCreate(node.id) : true); const rowClasses = [ styles.nodeRow, @@ -473,7 +449,7 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ tabIndex={-1} style={{ opacity: node.neutralize ? 1 : 0.35 }} > - {_NEUTRALIZE_EMOJI} + {node.neutralize ? _NEUTRALIZE_ON_EMOJI : _NEUTRALIZE_OFF_EMOJI} )} @@ -520,6 +496,9 @@ export function FormGeneratorTree({ const [filterText, setFilterText] = useState(''); /** Folders we expanded and confirmed have no visible children → hide chevron like a real leaf */ const [confirmedEmptyFolderIds, setConfirmedEmptyFolderIds] = useState(() => new Set()); + /** Per-node set of in-flight action keys (e.g. scope/neutralize/rag) so rows + * can render a spinner over the corresponding button. */ + const [pendingActions, setPendingActions] = useState>>(() => new Map()); const lastSelectedIdRef = useRef(null); const treeContentRef = useRef(null); /** Tracks node ids for which auto-expand has already fired (one-shot). */ @@ -667,25 +646,11 @@ export function FormGeneratorTree({ const _handleToggleExpand = useCallback( async (id: string) => { const wasExpanded = expandedIds.has(id); - const node = nodes.find((n) => n.id === id); - if (node && !wasExpanded) { - const childMap = _buildChildMap(nodes); - const existingChildren = childMap.get(id); - if (!existingChildren || existingChildren.length === 0) { - const childNodes = await provider.loadChildren(id, ownership); - if (childNodes.length > 0) { - setNodes((prev) => [...prev, ...childNodes]); - setConfirmedEmptyFolderIds((prev) => { - const next = new Set(prev); - next.delete(id); - return next; - }); - } else if (node.type === 'folder') { - setConfirmedEmptyFolderIds((prev) => new Set(prev).add(id)); - } - }; - _collectDescendants(id); + + if (wasExpanded) { + // Collapse: remove all descendants from nodes state and expandedIds. + const descendantIds = new Set(_collectDescendantIds(id, nodes)); setExpandedIds((prev) => { const next = new Set(prev); next.delete(id); @@ -693,17 +658,30 @@ export function FormGeneratorTree({ return next; }); setNodes((prev) => prev.filter((n) => !descendantIds.has(n.id))); - } else { - // Expand: load children from backend (always fresh). - setExpandedIds((prev) => new Set([...prev, id])); + return; + } + + // Expand: load children from backend (fresh) and track empty folders so + // we can hide the chevron for confirmed-empty ones. + setExpandedIds((prev) => new Set([...prev, id])); + const childMap = _buildChildMap(nodes); + const existingChildren = childMap.get(id); + if (!existingChildren || existingChildren.length === 0) { const childNodes = await provider.loadChildren(id, ownership); if (childNodes.length > 0) { setNodes((prev) => _mergeNodes(prev, childNodes)); + setConfirmedEmptyFolderIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + } else if (node?.type === 'folder') { + setConfirmedEmptyFolderIds((prev) => new Set(prev).add(id)); } - setTimeout(() => { - _scrollExpandedNodeToCenter(id); - }, 50); } + setTimeout(() => { + _scrollExpandedNodeToCenter(id); + }, 50); }, [nodes, expandedIds, provider, ownership, _mergeNodes], ); @@ -818,24 +796,18 @@ export function FormGeneratorTree({ if (!trimmed) return; try { const newNode = await provider.createChild(parentId, trimmed); -<<<<<<< HEAD setNodes((prev) => _mergeNodes(prev, [newNode])); // The provider may have re-parented `newNode` (e.g. onto a synth-root) // when `parentId === null`; expand whichever parent the resulting node // actually points at, so the new folder is visible. const visibleParent = newNode.parentId ?? null; if (visibleParent) { - setExpandedIds((prev) => new Set(prev).add(visibleParent)); -======= - setNodes((prev) => [...prev, newNode]); - if (parentId) { setConfirmedEmptyFolderIds((prev) => { const next = new Set(prev); - next.delete(parentId); + next.delete(visibleParent); return next; }); - setExpandedIds((prev) => new Set(prev).add(parentId)); ->>>>>>> ae63020 (finished file tree folder selection in file create node) + setExpandedIds((prev) => new Set(prev).add(visibleParent)); } } catch { await _handleRefresh(); @@ -1203,11 +1175,7 @@ export function FormGeneratorTree({ )} -<<<<<<< HEAD - {selectable && selectedIds.size > 0 && batchActions.length > 0 && ( -======= - {selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && ( ->>>>>>> 7fb9645 (workign on folder location in file create node) + {selectable && selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && (
{selectedIds.size} selected {batchActions.map((action: TreeBatchAction) => { diff --git a/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx b/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx index 6c3a132..58f800d 100644 --- a/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx @@ -56,6 +56,33 @@ function _mapFileToNode(file: FileData, ownership: Ownership): TreeNode { }; } +/** Stable synthetic root id per ownership scope. The real top-level + * folders/files attach their `parentId` to this id once we re-parent them + * in `loadChildren`. The id stays inside the FE provider; the backend + * never sees it. */ +const _SYNTH_ROOT_ID = (ownership: Ownership): string => `__filesRoot:${ownership}`; + +/** Build the synthetic root node. Its only job is to: + * - act as a drop-target for moving items back to top-level, + * - expose a global neutralize/scope toggle that cascades to every + * top-level descendant. + * Its scope/neutralize values are intentionally `undefined` (= "no own + * state") — the icons render an indeterminate state and a click sets the + * intent on every owned descendant. */ +function _makeSyntheticRoot(ownership: Ownership): TreeNode { + return { + id: _SYNTH_ROOT_ID(ownership), + name: '/', + type: 'folder', + parentId: null, + ownership, + icon: , + defaultExpanded: true, + scope: 'personal', + neutralize: false, + }; +} + export function createFolderFileProvider(options: { includeFiles?: boolean } = {}): TreeNodeProvider { const includeFiles = options.includeFiles !== false; const ownerParam = (ownership: Ownership) => (ownership === 'own' ? 'me' : 'shared'); @@ -127,14 +154,18 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = { const foldersRes = await api.get('/api/files/folders/tree', { params: { owner } }); const allFolders: FolderData[] = foldersRes.data ?? []; - const childFolders = allFolders.filter((f) => (f.parentId ?? null) === parentId); - nodes.push(...childFolders.map((f) => _mapFolderToNode(f, ownership, allFolders, includeFiles))); + const childFolders = allFolders.filter((f) => (f.parentId ?? null) === apiParentId); + const folderNodes = childFolders.map((f) => _mapFolderToNode(f, ownership, allFolders, includeFiles)); + if (apiParentId === null) { + for (const n of folderNodes) n.parentId = synthRootId; + } + nodes.push(...folderNodes); if (includeFiles) { try { const filters: Record = {}; - if (parentId) { - filters.folderId = parentId; + if (apiParentId) { + filters.folderId = apiParentId; } const paginationParam = JSON.stringify({ filters, pageSize: 500 }); const filesRes = await api.get('/api/files/list', { @@ -147,12 +178,16 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = { } else if (Array.isArray(data)) { rawFiles = data; } - let matched = rawFiles.filter((f) => (f.folderId ?? null) === parentId); + let matched = rawFiles.filter((f) => (f.folderId ?? null) === apiParentId); if (ownership === 'shared') { const myId = getUserDataCache()?.id; if (myId) matched = matched.filter((f) => f.sysCreatedBy !== myId); } - nodes.push(...matched.map((f) => _mapFileToNode(f, ownership))); + const fileNodes = matched.map((f) => _mapFileToNode(f, ownership)); + if (apiParentId === null) { + for (const n of fileNodes) n.parentId = synthRootId; + } + nodes.push(...fileNodes); } catch { // file list may fail for shared trees; folders still render } @@ -225,8 +260,8 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = { : targetParentId; await Promise.all( ids.map((id) => { - if (_isFile(id)) return api.put(`/api/files/${id}`, { folderId: targetParentId }); - return api.post(`/api/files/folders/${id}/move`, { parentId: targetParentId }); + if (_isFile(id)) return api.put(`/api/files/${id}`, { folderId: apiTarget }); + return api.post(`/api/files/folders/${id}/move`, { parentId: apiTarget }); }), ); }, diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 140c41e..9ebda48 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -10,6 +10,7 @@ import { Outlet, useLocation } from 'react-router-dom'; import { FeatureProvider, useFeatureStore } from '../stores/featureStore'; import { MandateNavigation } from '../components/Navigation/MandateNavigation'; import { UserSection } from '../components/Navigation/UserSection'; +import { RagRunningBadge } from '../components/RagRunningBadge/RagRunningBadge'; import { KEEP_ALIVE_ROUTES, hideFeatureOutlet } from '../config/keepAliveRoutes'; import type { KeepAliveEntry, KeepAliveScopedEntry, KeepAliveUnscopedEntry } from '../types/keepAlive.types'; import { isKeepAliveScoped } from '../types/keepAlive.types'; diff --git a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx index 554d564..a63e162 100644 --- a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx +++ b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx @@ -559,7 +559,6 @@ const TaskCard: React.FC = ({ dismissing = false, }) => { const { t } = useLanguage(); - const { request } = useApiRequest(); const { handleFileUpload } = useFileOperations(); const [formData, setFormData] = useState>({}); const [formPopupOpen, setFormPopupOpen] = useState(false);