From 9b0923b9da7b46c662e9cac2a6ab0517eb83fc43 Mon Sep 17 00:00:00 2001 From: Ida Date: Wed, 6 May 2026 08:37:45 +0200 Subject: [PATCH] workign on folder location in file create node --- .../FlowEditor/editor/NodeConfigPanel.tsx | 1 + .../UserFileFolderPicker.tsx | 115 ++++++++ .../nodes/frontendTypeRenderers/index.tsx | 2 + .../FormGeneratorTree.module.css | 16 ++ .../FormGeneratorTree/FormGeneratorTree.tsx | 267 ++++++++---------- .../providers/FolderFileProvider.tsx | 82 ++---- .../FormGenerator/FormGeneratorTree/types.ts | 6 + 7 files changed, 285 insertions(+), 204 deletions(-) create mode 100644 src/components/FlowEditor/nodes/frontendTypeRenderers/UserFileFolderPicker.tsx diff --git a/src/components/FlowEditor/editor/NodeConfigPanel.tsx b/src/components/FlowEditor/editor/NodeConfigPanel.tsx index ce6f3f1..daabb62 100644 --- a/src/components/FlowEditor/editor/NodeConfigPanel.tsx +++ b/src/components/FlowEditor/editor/NodeConfigPanel.tsx @@ -320,6 +320,7 @@ const _LEGACY_RENDERERS_THAT_HANDLE_BINDINGS = new Set([ 'featureInstance', 'sharepointFolder', 'sharepointFile', + 'userFileFolder', 'clickupList', 'clickupTask', 'dataRef', diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/UserFileFolderPicker.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/UserFileFolderPicker.tsx new file mode 100644 index 0000000..76b4c0a --- /dev/null +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/UserFileFolderPicker.tsx @@ -0,0 +1,115 @@ +/** + * userFileFolder — same folder tree as Meine Dateien (FormGeneratorTree) inside a collapsible panel. + */ + +import React, { useMemo, useCallback, useState } from 'react'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import { FormGeneratorTree } from '../../../FormGenerator/FormGeneratorTree'; +import { createFolderFileProvider } from '../../../FormGenerator/FormGeneratorTree/providers/FolderFileProvider'; +import type { TreeNode } from '../../../FormGenerator/FormGeneratorTree'; +import type { FieldRendererProps } from './index'; + +export const UserFileFolderPicker: React.FC = ({ param, value, onChange, request }) => { + const { t } = useLanguage(); + const [panelOpen, setPanelOpen] = useState(true); + + const provider = useMemo(() => createFolderFileProvider({ includeFiles: false }), []); + + const strVal = typeof value === 'string' ? value : ''; + const rootSelected = strVal === ''; + + const handleNodeClick = useCallback( + (node: TreeNode) => { + if (node.type === 'folder') { + onChange(node.id); + } + }, + [onChange], + ); + + return ( +
+ + {!request && ( +
{t('Ordnerliste nicht verfügbar (keine API-Anbindung).')}
+ )} + {request && ( + <> + + + {panelOpen && ( +
+
onChange('')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onChange(''); + } + }} + style={{ + padding: '8px 12px', + fontSize: 12, + fontWeight: 600, + cursor: 'pointer', + borderBottom: '1px solid var(--color-border, #e2e8f0)', + background: rootSelected + ? 'rgba(37, 99, 235, 0.12)' + : 'var(--table-header-bg, #f8fafc)', + }} + > + {t('Stamm — Meine Dateien')} +
+ +
+ )} + + )} +
+ ); +}; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx index 7ac9d92..e8e8922 100644 --- a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -34,6 +34,7 @@ import type { CanvasNode } from '../../editor/FlowCanvas'; import { DataRefRenderer } from './DataRefRenderer'; import { ContextBuilderRenderer } from './ContextBuilderRenderer'; import { FeatureInstancePicker } from './FeatureInstancePicker'; +import { UserFileFolderPicker } from './UserFileFolderPicker'; import { TemplateTextareaRenderer } from './TemplateTextareaRenderer'; import { getApiBaseUrl } from '../../../../../config/config'; @@ -917,6 +918,7 @@ export const FRONTEND_TYPE_RENDERERS: Record = { featureInstance: FeatureInstancePicker, sharepointFolder: SharepointPathPicker, sharepointFile: SharepointPathPicker, + userFileFolder: UserFileFolderPicker, clickupList: FolderPicker, clickupTask: FolderPicker, caseList: CaseListEditor, diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css index 1c3fba4..bb2cdb1 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css @@ -543,6 +543,22 @@ line-height: 1.5; } +/* Embedded workflow / compact pickers — fixed height so flex children (treeWrapper) get a real viewport */ +.embeddedPicker { + display: flex; + flex-direction: column; + flex: none !important; + min-height: 0; + overflow: hidden; + /* height + maxHeight set inline (embedMaxHeight) */ +} + +.embeddedPicker .treeWrapper { + flex: 1 1 0; + min-height: 0; + max-height: none; +} + /* Compact mode */ .compactMode .sectionHeader { padding: 6px 8px; diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx index 2342e13..13155bb 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx @@ -180,6 +180,7 @@ interface TreeNodeRowProps { onDragOver: (e: React.DragEvent, node: TreeNode) => void; onDragLeave: (e: React.DragEvent) => void; onDrop: (e: React.DragEvent, node: TreeNode) => void; + hideRowActionButtons?: boolean; } const TreeNodeRow = React.memo(function TreeNodeRow({ @@ -213,6 +214,7 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ onDragOver, onDragLeave, onDrop, + hideRowActionButtons = false, }: TreeNodeRowProps) { const { node, depth, hasChildren } = entry; const renameRef = useRef(null); @@ -246,11 +248,12 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ const _handleDoubleClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); + if (hideRowActionButtons) return; if (ownership === 'own' && provider.canRename?.(node)) { onStartRename(node.id); } }, - [ownership, provider, node, onStartRename], + [hideRowActionButtons, ownership, provider, node, onStartRename], ); const _handleRowClick = useCallback( @@ -298,11 +301,11 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ className={rowClasses} onClick={_handleRowClick} onDoubleClick={_handleDoubleClick} - draggable - onDragStart={(e) => onDragStart(e, node)} - onDragOver={(e) => onDragOver(e, node)} - onDragLeave={onDragLeave} - onDrop={(e) => onDrop(e, node)} + draggable={!hideRowActionButtons} + onDragStart={hideRowActionButtons ? undefined : (e) => onDragStart(e, node)} + onDragOver={hideRowActionButtons ? undefined : (e) => onDragOver(e, node)} + onDragLeave={hideRowActionButtons ? undefined : onDragLeave} + onDrop={hideRowActionButtons ? undefined : (e) => onDrop(e, node)} data-node-id={node.id} title={node.name} role="treeitem" @@ -312,7 +315,7 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ >
- {selectable && ( + {!hideRowActionButtons && ( ({ )} -
+ {!hideRowActionButtons && ( {node.sizeBytes != null ? _formatSize(node.sizeBytes) : ''} + )} -
- {canCreateChild && onCreateChild && ( - - )} + {!hideRowActionButtons && ( + <> +
+ {canRename && ( + + )} - {canRename && ( - - )} + {node.type !== 'folder' && ( + + )} - {node.type !== 'folder' && provider.downloadNode && ( - - )} + {canDelete && ( + + )} +
- {canDelete && ( - - )} -
-
+
+ {onSendToChat && ( + + )} -
- {/* Order (left-to-right): extraActions (e.g. settings) -> RAG -> sendToChat -> scope -> neutralize. */} - {node.extraActions?.map((action) => ( - - ))} + {node.scope !== undefined && ( + + )} - {node.ragIndexEnabled !== undefined && ( - - )} - - {onSendToChat && ( - - )} - - {node.scope !== undefined && ( - - )} - - {node.neutralize !== undefined && ( - - )} -
+ {node.neutralize !== undefined && ( + + )} +
+ + )}
); }) as (props: TreeNodeRowProps) => React.ReactElement; @@ -532,6 +483,8 @@ export function FormGeneratorTree({ selectable = true, refreshAfterAction = false, className, + embedMaxHeight, + hideRowActionButtons = false, }: FormGeneratorTreeProps) { const { t } = useLanguage(); const { confirm, ConfirmDialog } = useConfirm(); @@ -1097,6 +1050,7 @@ export function FormGeneratorTree({ } case 'F2': { e.preventDefault(); + if (hideRowActionButtons) break; const node = nodes.find((n) => n.id === focusedId); if (node && ownership === 'own' && provider.canRename?.(node)) { _handleStartRename(focusedId); @@ -1105,6 +1059,7 @@ export function FormGeneratorTree({ } case 'Delete': { e.preventDefault(); + if (hideRowActionButtons) break; const node = nodes.find((n) => n.id === focusedId); if (node && ownership === 'own' && provider.canDelete?.(node)) { _handleDelete(focusedId); @@ -1124,6 +1079,7 @@ export function FormGeneratorTree({ _handleToggleSelect, _handleStartRename, _handleDelete, + hideRowActionButtons, ], ); @@ -1163,13 +1119,21 @@ export function FormGeneratorTree({ const wrapperClasses = [ styles.formGeneratorTree, compact && styles.compactMode, + embedMaxHeight != null && styles.embeddedPicker, className, ] .filter(Boolean) .join(' '); return ( -
+
{title && (
({
)} +<<<<<<< HEAD {selectable && selectedIds.size > 0 && batchActions.length > 0 && ( +======= + {selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && ( +>>>>>>> 7fb9645 (workign on folder location in file create node)
{selectedIds.size} selected {batchActions.map((action: TreeBatchAction) => { @@ -1318,6 +1286,7 @@ export function FormGeneratorTree({ onDragOver={_handleDragOver} onDragLeave={_handleDragLeave} onDrop={_handleDrop} + hideRowActionButtons={hideRowActionButtons} /> )) )} diff --git a/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx b/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx index 810b686..c96a4c4 100644 --- a/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx @@ -52,34 +52,8 @@ 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(): TreeNodeProvider { +export function createFolderFileProvider(options: { includeFiles?: boolean } = {}): TreeNodeProvider { + const includeFiles = options.includeFiles !== false; const ownerParam = (ownership: Ownership) => (ownership === 'own' ? 'me' : 'shared'); const typeMap = new Map(); @@ -157,34 +131,32 @@ export function createFolderFileProvider(): TreeNodeProvider { } nodes.push(...folderNodes); - try { - const filters: Record = {}; - if (apiParentId) { - filters.folderId = apiParentId; + if (includeFiles) { + try { + const filters: Record = {}; + if (parentId) { + filters.folderId = parentId; + } + const paginationParam = JSON.stringify({ filters, pageSize: 500 }); + const filesRes = await api.get('/api/files/list', { + params: { pagination: paginationParam }, + }); + const data = filesRes.data; + let rawFiles: FileData[] = []; + if (data && typeof data === 'object' && 'items' in data) { + rawFiles = Array.isArray(data.items) ? data.items : []; + } else if (Array.isArray(data)) { + rawFiles = data; + } + let matched = rawFiles.filter((f) => (f.folderId ?? null) === parentId); + if (ownership === 'shared') { + const myId = getUserDataCache()?.id; + if (myId) matched = matched.filter((f) => f.sysCreatedBy !== myId); + } + nodes.push(...matched.map((f) => _mapFileToNode(f, ownership))); + } catch { + // file list may fail for shared trees; folders still render } - const paginationParam = JSON.stringify({ filters, pageSize: 500 }); - const filesRes = await api.get('/api/files/list', { - params: { pagination: paginationParam }, - }); - const data = filesRes.data; - let rawFiles: FileData[] = []; - if (data && typeof data === 'object' && 'items' in data) { - rawFiles = Array.isArray(data.items) ? data.items : []; - } else if (Array.isArray(data)) { - rawFiles = data; - } - 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); - } - 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 } _trackTypes(nodes); diff --git a/src/components/FormGenerator/FormGeneratorTree/types.ts b/src/components/FormGenerator/FormGeneratorTree/types.ts index 563a4a3..9762e9b 100644 --- a/src/components/FormGenerator/FormGeneratorTree/types.ts +++ b/src/components/FormGenerator/FormGeneratorTree/types.ts @@ -122,4 +122,10 @@ export interface FormGeneratorTreeProps { * that rely on the optimistic-update path. */ refreshAfterAction?: boolean; className?: string; + /** Embedded pickers (e.g. automation node config): constrain overall height so the tree scrolls inside. */ + embedMaxHeight?: number; + /** + * Hides checkbox, size column, per-row emoji actions, drag-drop, and batch toolbar — saves space in pickers. + */ + hideRowActionButtons?: boolean; }