diff --git a/src/components/Automation2FlowEditor/NodeSidebar.tsx b/src/components/Automation2FlowEditor/NodeSidebar.tsx index 80a768e..14d8707 100644 --- a/src/components/Automation2FlowEditor/NodeSidebar.tsx +++ b/src/components/Automation2FlowEditor/NodeSidebar.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; import { FaChevronDown, FaChevronRight } from 'react-icons/fa'; import type { NodeType, NodeTypeCategory } from '../../api/automation2Api'; -import { CATEGORY_ORDER } from './constants'; +import { CATEGORY_ORDER, HIDDEN_NODE_IDS } from './constants'; import { getLabel } from './utils'; import { NodeListItem } from './NodeListItem'; import styles from './Automation2FlowEditor.module.css'; @@ -31,9 +31,10 @@ export const NodeSidebar: React.FC = ({ onToggleCategory, }) => { const filteredNodeTypes = useMemo(() => { - if (!filter.trim()) return nodeTypes; + const visible = nodeTypes.filter((n) => !HIDDEN_NODE_IDS.has(n.id)); + if (!filter.trim()) return visible; const q = filter.toLowerCase(); - return nodeTypes.filter( + return visible.filter( (n) => n.id.toLowerCase().includes(q) || getLabel(n.label, language).toLowerCase().includes(q) || diff --git a/src/components/Automation2FlowEditor/configs/SharePointNodeConfig.tsx b/src/components/Automation2FlowEditor/configs/SharePointNodeConfig.tsx index 15adbb9..9f03daf 100644 --- a/src/components/Automation2FlowEditor/configs/SharePointNodeConfig.tsx +++ b/src/components/Automation2FlowEditor/configs/SharePointNodeConfig.tsx @@ -1,10 +1,12 @@ /** * SharePoint node config - connection selector, path, search query. + * Uses SharepointBrowseTree (FolderTree-style) for file selection. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import type { NodeConfigRendererProps } from './types'; import { fetchConnections, fetchBrowse, type UserConnection, type BrowseEntry } from '../../../api/automation2Api'; +import { SharepointBrowseTree } from '../../FolderTree/SharepointBrowseTree'; export const SharePointNodeConfig: React.FC = ({ params, @@ -14,43 +16,62 @@ export const SharePointNodeConfig: React.FC = ({ nodeType = 'sharepoint.findFile', }) => { const [connections, setConnections] = useState([]); - const [browseItems, setBrowseItems] = useState([]); - const [currentPath, setCurrentPath] = useState('/'); - const [loading, setLoading] = useState(false); + const [browseExpanded, setBrowseExpanded] = useState(false); + const [copySourceExpanded, setCopySourceExpanded] = useState(false); + const [copyDestExpanded, setCopyDestExpanded] = useState(false); + const [connectionsLoading, setConnectionsLoading] = useState(false); const connectionId = (params.connectionId as string) ?? ''; - const path = (params.path as string) ?? '/'; + const pathParam = 'path'; + const path = (params.path as string) ?? (params.filePath as string) ?? ''; useEffect(() => { if (instanceId && request) { - setLoading(true); + setConnectionsLoading(true); fetchConnections(request, instanceId) .then(setConnections) .catch(() => setConnections([])) - .finally(() => setLoading(false)); + .finally(() => setConnectionsLoading(false)); } }, [instanceId, request]); - useEffect(() => { - if (instanceId && request && connectionId) { - const service = 'sharepoint'; - fetchBrowse(request, instanceId, connectionId, service, currentPath) - .then((r: { items: BrowseEntry[]; path: string; service: string }) => { - setBrowseItems(r.items); - setCurrentPath(r.path); - }) - .catch(() => setBrowseItems([])); - } else { - setBrowseItems([]); - } - }, [instanceId, request, connectionId, currentPath]); + const loadChildren = useCallback( + async (pathToLoad: string): Promise => { + if (!instanceId || !request || !connectionId) return []; + const r = await fetchBrowse(request, instanceId, connectionId, 'sharepoint', pathToLoad); + return r?.items ?? []; + }, + [instanceId, request, connectionId] + ); - const navigateTo = (entryPath: string) => setCurrentPath(entryPath); - const selectPath = (p: string) => updateParam('path', p); + const selectPath = useCallback( + (p: string) => { + updateParam(pathParam, p); + setBrowseExpanded(false); + }, + [updateParam, pathParam] + ); + + const selectSourcePath = useCallback( + (p: string) => { + updateParam('sourcePath', p); + setCopySourceExpanded(false); + }, + [updateParam] + ); + + const selectDestPath = useCallback( + (p: string) => { + updateParam('destPath', p); + setCopyDestExpanded(false); + }, + [updateParam] + ); const needsPath = !['sharepoint.findFile'].includes(nodeType); const needsSearch = nodeType === 'sharepoint.findFile'; - const needsSiteId = ['sharepoint.uploadFile', 'sharepoint.downloadFile', 'sharepoint.copyFile'].includes(nodeType); + const needsSiteId = false; + const hasPathInput = ['sharepoint.readFile', 'sharepoint.uploadFile', 'sharepoint.downloadFile', 'sharepoint.copyFile'].includes(nodeType); return ( <> @@ -58,13 +79,10 @@ export const SharePointNodeConfig: React.FC = ({ - updateParam(nodeType === 'sharepoint.downloadFile' ? 'filePath' : 'path', e.target.value) + onChange={(e) => updateParam('path', e.target.value)} + placeholder={ + nodeType === 'sharepoint.downloadFile' + ? '/sites/SiteName/Shared Documents/file.pdf' + : nodeType === 'sharepoint.uploadFile' + ? '/sites/.../Shared Documents/TargetFolder/' + : 'File or folder path' } - placeholder="File or folder path" /> )} @@ -114,76 +136,109 @@ export const SharePointNodeConfig: React.FC = ({ /> )} - {nodeType === 'sharepoint.uploadFile' && ( -
- - updateParam('fileName', e.target.value)} - placeholder="file.pdf" - /> -
- )} {nodeType === 'sharepoint.copyFile' && ( <> -
- - updateParam('sourceFolder', e.target.value)} - placeholder="Source folder path" - /> -
updateParam('sourceFile', e.target.value)} - placeholder="Source file name" + value={(params.sourcePath as string) ?? ''} + onChange={(e) => updateParam('sourcePath', e.target.value)} + placeholder="/sites/.../folder/file.pdf" />
- + updateParam('destFolder', e.target.value)} - placeholder="Destination folder path" - /> -
-
- - updateParam('destFile', e.target.value)} - placeholder="Destination file name" + value={(params.destPath as string) ?? ''} + onChange={(e) => updateParam('destPath', e.target.value)} + placeholder="/sites/.../target-folder/" />
+ {connectionId && ( + <> +
setCopySourceExpanded((e.target as HTMLDetailsElement).open)} + style={{ + marginTop: 12, + border: '1px solid var(--border-color, #e0e0e0)', + borderRadius: 6, + background: 'var(--bg-secondary, #f8f9fa)', + overflow: 'hidden', + }} + > + + πŸ“‚ Source file durchsuchen + +
+ +
+
+
setCopyDestExpanded((e.target as HTMLDetailsElement).open)} + style={{ + marginTop: 8, + border: '1px solid var(--border-color, #e0e0e0)', + borderRadius: 6, + background: 'var(--bg-secondary, #f8f9fa)', + overflow: 'hidden', + }} + > + + πŸ“‚ Zielordner durchsuchen + +
+ {}} onSelectFolder={selectDestPath} selectedPath={(params.destPath as string) || null} /> +
+
+ + )} )} - {connectionId && needsPath && ( -
- -
    - {currentPath !== '/' && ( -
  • - -
  • - )} - {browseItems.map((e) => ( -
  • - -
  • - ))} -
-
+ {connectionId && needsPath && hasPathInput && !['sharepoint.copyFile'].includes(nodeType) && ( +
setBrowseExpanded((e.target as HTMLDetailsElement).open)} + style={{ + marginTop: 12, + border: '1px solid var(--border-color, #e0e0e0)', + borderRadius: 6, + background: 'var(--bg-secondary, #f8f9fa)', + overflow: 'hidden', + }} + > + + πŸ“‚ + SharePoint durchsuchen + +
+ +
+
)} ); diff --git a/src/components/Automation2FlowEditor/constants.ts b/src/components/Automation2FlowEditor/constants.ts index e3da02b..38859c9 100644 --- a/src/components/Automation2FlowEditor/constants.ts +++ b/src/components/Automation2FlowEditor/constants.ts @@ -3,6 +3,29 @@ * Category ordering for node sidebar. */ +/** Node type IDs hidden from the sidebar (hidden, not removed – still work when present in saved graphs) */ +export const HIDDEN_NODE_IDS = new Set([ + 'trigger.schedule', // zeitplan + 'trigger.formSubmit', // formular-absendung + 'flow.ifElse', + 'flow.switch', + 'flow.merge', + 'flow.loop', + 'flow.wait', + 'flow.stop', // alle abschnitt ablauf + 'data.setFields', + 'data.filter', + 'data.parseJson', + 'data.template', // alle abschnitt daten + 'ai.webResearch', + 'ai.summarizeDocument', + 'ai.translateDocument', + 'ai.convertDocument', + 'ai.generateDocument', + 'ai.generateCode', // alle KI ausser ai.prompt + 'sharepoint.listFiles', // dateien auflisten +]); + /** Default category display order */ export const CATEGORY_ORDER = [ 'trigger', diff --git a/src/components/FolderTree/SharepointBrowseTree.tsx b/src/components/FolderTree/SharepointBrowseTree.tsx new file mode 100644 index 0000000..2b2c4d7 --- /dev/null +++ b/src/components/FolderTree/SharepointBrowseTree.tsx @@ -0,0 +1,307 @@ +/** + * SharepointBrowseTree – Lazy-loading tree for SharePoint browse. + * Same look & feel as FolderTree (chevron, FaFolder/FaFolderOpen, styling). + * Loads children on expand via onLoadChildren(path). + */ + +import React, { useState, useCallback, useEffect } from 'react'; +import { FaFolder, FaFolderOpen, FaChevronRight, FaGlobe } from 'react-icons/fa'; +import styles from './FolderTree.module.css'; + +export interface BrowseEntry { + name: string; + path: string; + isFolder: boolean; + size?: number; + mimeType?: string; + metadata?: Record; +} + +export interface SharepointBrowseTreeProps { + /** Root path (usually "/") - children loaded via onLoadChildren */ + rootPath?: string; + /** Load children for a given path. Returns folders and files. */ + onLoadChildren: (path: string) => Promise; + /** Called when user selects a file path */ + onSelectFile: (path: string) => void; + /** Called when user selects a folder path (e.g. for destination). If provided, folder rows are selectable. */ + onSelectFolder?: (path: string) => void; + /** Currently selected path (for highlight) */ + selectedPath?: string | null; + /** Optional: pre-seed root children (e.g. from initial load) */ + initialChildren?: BrowseEntry[]; +} + +function _fileIcon(mime?: string): string { + if (!mime) return '\uD83D\uDCC4'; + if (mime.startsWith('image/')) return '\uD83D\uDDBC\uFE0F'; + if (mime.includes('pdf')) return '\uD83D\uDCD5'; + if (mime.includes('word') || mime.includes('docx')) return '\uD83D\uDCD8'; + if (mime.includes('sheet') || mime.includes('xlsx') || mime.includes('csv')) return '\uD83D\uDCCA'; + if (mime.includes('presentation') || mime.includes('pptx')) return '\uD83D\uDCD9'; + if (mime.includes('zip') || mime.includes('tar') || mime.includes('gz')) return '\uD83D\uDCE6'; + if (mime.startsWith('text/') || mime.includes('json') || mime.includes('xml')) return '\uD83D\uDCDD'; + return '\uD83D\uDCC4'; +} + +/* ── File row ──────────────────────────────────────────────────────────── */ + +function _FileRow({ + entry, + selectedPath, + onSelect, +}: { + entry: BrowseEntry; + selectedPath: string | null | undefined; + onSelect: (path: string) => void; +}) { + const isSelected = selectedPath === entry.path; + + return ( +
onSelect(entry.path)} + title={entry.path} + > + + {_fileIcon(entry.mimeType)} + {entry.name} + {entry.size != null && ( + + {(entry.size / 1024).toFixed(0)}K + + )} +
+ ); +} + +/* ── Folder row (expandable, lazy-loads children) ───────────────────────── */ + +function _FolderRow({ + entry, + selectedPath, + expandedPaths, + loadedChildren, + loadingPaths, + onToggle, + onSelectFile, + onSelectFolder, +}: { + entry: BrowseEntry; + selectedPath: string | null | undefined; + expandedPaths: Set; + loadedChildren: Record; + loadingPaths: Set; + onToggle: (path: string) => void; + onSelectFile: (path: string) => void; + onSelectFolder?: (path: string) => void; +}) { + const isExpanded = expandedPaths.has(entry.path); + const isSelected = selectedPath === entry.path; + const children = loadedChildren[entry.path] ?? []; + const folders = children.filter((c) => c.isFolder).sort((a, b) => a.name.localeCompare(b.name)); + const files = children.filter((c) => !c.isFolder).sort((a, b) => a.name.localeCompare(b.name)); + const hasChildren = folders.length > 0 || files.length > 0; + const isLoading = isExpanded && loadingPaths.has(entry.path); + + const handleRowClick = (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + if (target.closest(`.${styles.chevron}`)) return; + if (onSelectFolder) { + onSelectFolder(entry.path); + return; + } + onToggle(entry.path); + }; + + const handleChevronClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onToggle(entry.path); + }; + + return ( +
+
+ + + + + {isExpanded ? : } + + {entry.name} + {isLoading && ( + … + )} +
+ {isExpanded && ( +
+ {isLoading ? ( +
+ Wird geladen… +
+ ) : ( + <> + {folders.map((child) => ( + <_FolderRow + key={child.path} + entry={child} + selectedPath={selectedPath} + expandedPaths={expandedPaths} + loadedChildren={loadedChildren} + loadingPaths={loadingPaths} + onToggle={onToggle} + onSelectFile={onSelectFile} + onSelectFolder={onSelectFolder} + /> + ))} + {files.map((child) => ( + <_FileRow + key={child.path} + entry={child} + selectedPath={selectedPath} + onSelect={onSelectFile} + /> + ))} + {children.length === 0 && ( +
+ Leer +
+ )} + + )} +
+ )} +
+ ); +} + +/* ── Root component ─────────────────────────────────────────────────────── */ + +export function SharepointBrowseTree({ + rootPath = '/', + onLoadChildren, + onSelectFile, + onSelectFolder, + selectedPath, + initialChildren = [], +}: SharepointBrowseTreeProps) { + const [expandedPaths, setExpandedPaths] = useState>(new Set([rootPath])); + const [loadedChildren, setLoadedChildren] = useState>(() => + initialChildren.length > 0 ? { [rootPath]: initialChildren } : {} + ); + const [loadingPaths, setLoadingPaths] = useState>(new Set()); + + const loadPath = useCallback( + async (path: string) => { + setLoadingPaths((p) => new Set(p).add(path)); + try { + const items = await onLoadChildren(path); + setLoadedChildren((prev) => ({ ...prev, [path]: items })); + } catch { + setLoadedChildren((prev) => ({ ...prev, [path]: [] })); + } finally { + setLoadingPaths((p) => { + const next = new Set(p); + next.delete(path); + return next; + }); + } + }, + [onLoadChildren] + ); + + const handleToggle = useCallback( + (path: string) => { + setExpandedPaths((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + loadPath(path); + } + return next; + }); + }, + [loadPath] + ); + + useEffect(() => { + if (rootPath in loadedChildren) return; + if (initialChildren.length > 0) return; + loadPath(rootPath); + }, [rootPath, initialChildren.length, loadPath]); + + const rootItems = loadedChildren[rootPath] ?? []; + const rootLoading = loadingPaths.has(rootPath); + const rootFolders = rootItems.filter((e) => e.isFolder).sort((a, b) => a.name.localeCompare(b.name)); + const rootFiles = rootItems.filter((e) => !e.isFolder).sort((a, b) => a.name.localeCompare(b.name)); + const isRootExpanded = expandedPaths.has(rootPath); + + return ( +
+
+ handleToggle(rootPath)} + > + + + + SharePoint + {rootLoading && ( + … + )} +
+ {isRootExpanded && ( +
+ {rootLoading ? ( +
+ Sites werden geladen… +
+ ) : ( + <> + {rootFolders.map((entry) => ( + <_FolderRow + key={entry.path} + entry={entry} + selectedPath={selectedPath} + expandedPaths={expandedPaths} + loadedChildren={loadedChildren} + loadingPaths={loadingPaths} + onToggle={handleToggle} + onSelectFile={onSelectFile} + onSelectFolder={onSelectFolder} + /> + ))} + {rootFiles.map((entry) => ( + <_FileRow + key={entry.path} + entry={entry} + selectedPath={selectedPath} + onSelect={onSelectFile} + /> + ))} + {rootItems.length === 0 && !rootLoading && ( +
+ Keine EintrΓ€ge +
+ )} + + )} +
+ )} +
+ ); +}