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 97a8592..c1ba1a8 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css @@ -503,6 +503,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 5b9f92e..01b60d7 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx @@ -133,6 +133,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({ @@ -161,6 +162,7 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ onDragOver, onDragLeave, onDrop, + hideRowActionButtons = false, }: TreeNodeRowProps) { const { node, depth, hasChildren } = entry; const renameRef = useRef(null); @@ -194,11 +196,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( @@ -240,11 +243,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" @@ -254,17 +257,19 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ >
- {}} - onClick={(e) => { - e.stopPropagation(); - onToggleSelect(node.id, e as unknown as React.MouseEvent); - }} - tabIndex={-1} - /> + {!hideRowActionButtons && ( + {}} + onClick={(e) => { + e.stopPropagation(); + onToggleSelect(node.id, e as unknown as React.MouseEvent); + }} + tabIndex={-1} + /> + )} {hasChildren ? ( ({ )} - - {node.sizeBytes != null ? _formatSize(node.sizeBytes) : ''} - + {!hideRowActionButtons && ( + + {node.sizeBytes != null ? _formatSize(node.sizeBytes) : ''} + + )} -
- {canRename && ( - - )} + {!hideRowActionButtons && ( + <> +
+ {canRename && ( + + )} - {node.type !== 'folder' && ( - - )} + {node.type !== 'folder' && ( + + )} - {canDelete && ( - - )} -
+ {canDelete && ( + + )} +
-
- {onSendToChat && ( - - )} +
+ {onSendToChat && ( + + )} - {node.scope !== undefined && ( - - )} + {node.scope !== undefined && ( + + )} - {node.neutralize !== undefined && ( - - )} -
+ {node.neutralize !== undefined && ( + + )} +
+ + )}
); }) as (props: TreeNodeRowProps) => React.ReactElement; @@ -403,6 +423,8 @@ export function FormGeneratorTree({ onSendToChat, allowCreateFolder = true, className, + embedMaxHeight, + hideRowActionButtons = false, }: FormGeneratorTreeProps) { const { prompt, PromptDialog } = usePrompt(); const [nodes, setNodes] = useState[]>([]); @@ -784,6 +806,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); @@ -792,6 +815,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); @@ -811,6 +835,7 @@ export function FormGeneratorTree({ _handleToggleSelect, _handleStartRename, _handleDelete, + hideRowActionButtons, ], ); @@ -850,13 +875,21 @@ export function FormGeneratorTree({ const wrapperClasses = [ styles.formGeneratorTree, compact && styles.compactMode, + embedMaxHeight != null && styles.embeddedPicker, className, ] .filter(Boolean) .join(' '); return ( -
+
{title && (
({
)} - {selectedIds.size > 0 && batchActions.length > 0 && ( + {selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && (
{selectedIds.size} selected {batchActions.map((action: TreeBatchAction) => { @@ -996,6 +1029,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 7e8b6f7..1857340 100644 --- a/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider.tsx @@ -52,7 +52,8 @@ function _mapFileToNode(file: FileData, ownership: Ownership): TreeNode { }; } -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(); @@ -78,30 +79,32 @@ export function createFolderFileProvider(): TreeNodeProvider { const childFolders = allFolders.filter((f) => (f.parentId ?? null) === parentId); nodes.push(...childFolders.map((f) => _mapFolderToNode(f, ownership))); - try { - const filters: Record = {}; - if (parentId) { - filters.folderId = parentId; + 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) === 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 } _trackTypes(nodes); diff --git a/src/components/FormGenerator/FormGeneratorTree/types.ts b/src/components/FormGenerator/FormGeneratorTree/types.ts index d715033..5ad75c4 100644 --- a/src/components/FormGenerator/FormGeneratorTree/types.ts +++ b/src/components/FormGenerator/FormGeneratorTree/types.ts @@ -63,4 +63,10 @@ export interface FormGeneratorTreeProps { /** When false, hides "Neuer Ordner" (e.g. map from table file permissions). Default true. */ allowCreateFolder?: 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; }