fix: node inhalt extrahieren nimmt jetzt context, files page formgenerator und folder tree zeigen jetzt die gleichen elemente

This commit is contained in:
Ida 2026-05-26 12:03:53 +02:00
parent 1c539076e5
commit 9d081e8819
5 changed files with 143 additions and 27 deletions

View file

@ -36,6 +36,7 @@ export interface PaginationParams {
search?: string; search?: string;
viewKey?: string; viewKey?: string;
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>; groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
owner?: 'all' | 'me' | 'shared';
} }
export interface PaginatedResponse<T> { export interface PaginatedResponse<T> {
@ -109,6 +110,7 @@ export async function fetchFiles(
if (params.search) paginationObj.search = params.search; if (params.search) paginationObj.search = params.search;
if (params.viewKey) paginationObj.viewKey = params.viewKey; if (params.viewKey) paginationObj.viewKey = params.viewKey;
if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels; if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
if (params.owner) requestParams.owner = params.owner;
if (Object.keys(paginationObj).length > 0) { if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj); requestParams.pagination = JSON.stringify(paginationObj);

View file

@ -9,6 +9,7 @@ import type { GraphDefinedSchemaRef, NodeType, NodeTypeParameter, PortSchema } f
import type { ApiRequestFunction } from '../../../api/workflowApi'; import type { ApiRequestFunction } from '../../../api/workflowApi';
import { getLabel } from '../nodes/shared/utils'; import { getLabel } from '../nodes/shared/utils';
import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers'; import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers';
import { ContextBuilderRenderer } from '../nodes/frontendTypeRenderers/ContextBuilderRenderer';
import { RequiredAttributePicker } from '../nodes/shared/RequiredAttributePicker'; import { RequiredAttributePicker } from '../nodes/shared/RequiredAttributePicker';
import { findRequiredErrors } from '../nodes/shared/paramValidation'; import { findRequiredErrors } from '../nodes/shared/paramValidation';
import { useAutomation2DataFlow } from '../context/Automation2DataFlowContext'; import { useAutomation2DataFlow } from '../context/Automation2DataFlowContext';
@ -253,6 +254,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
for (const param of sortedParameters) { for (const param of sortedParameters) {
if (param.frontendType === 'hidden') continue; if (param.frontendType === 'hidden') continue;
if (param.name === 'context') continue;
if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue; if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue;
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue; if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue;
@ -378,6 +380,15 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
t, t,
]); ]);
const extractContentContextParam = useMemo((): NodeTypeParameter | null => {
if (!node || !nodeType || node.type !== CONTEXT_EXTRACT_CONTENT_NODE_TYPE) return null;
const param = sortedParameters.find((p) => p.name === 'context') ?? null;
if (!param) return null;
if (param.frontendType === 'hidden') return null;
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) return null;
return param;
}, [node, nodeType, sortedParameters, params]);
if (!node || !nodeType) return null; if (!node || !nodeType) return null;
const isTrigger = node.type.startsWith('trigger.'); const isTrigger = node.type.startsWith('trigger.');
@ -483,11 +494,71 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
</div> </div>
)} )}
{extractContentAccordionItems !== null ? ( {extractContentAccordionItems !== null ? (
<>
{extractContentContextParam ? (
<div
key={`${node.id}-${extractContentContextParam.name}`}
style={{ marginBottom: 8, minWidth: 0, maxWidth: '100%' }}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
marginBottom: 2,
flexWrap: 'wrap',
minWidth: 0,
}}
>
{extractContentContextParam.required && (
<span
title={t('Pflichtfeld')}
style={{ color: 'var(--danger-color, #dc3545)', fontWeight: 700, flexShrink: 0 }}
>
*
</span>
)}
{verboseSchema && extractContentContextParam.type && (
<span
title={t('Parameter-Typ')}
style={{
fontSize: 10,
fontWeight: 600,
color: 'var(--text-secondary)',
background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)',
borderRadius: 4,
padding: '1px 6px',
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{extractContentContextParam.type}
</span>
)}
</div>
<ContextBuilderRenderer
param={extractContentContextParam}
value={workflowParamUiValue(params, extractContentContextParam)}
onChange={(val: unknown) => updateParam(extractContentContextParam.name, val)}
allParams={params}
instanceId={instanceId}
request={request}
nodeType={node.type}
onPatchParams={patchParams}
/>
</div>
) : null}
{extractContentAccordionItems.length > 0 ? (
<AccordionList<string> <AccordionList<string>
key={`${node.id}-extract-accordion`} key={`${node.id}-extract-accordion`}
defaultOpenId={null} defaultOpenId={null}
items={extractContentAccordionItems} items={extractContentAccordionItems}
/> />
) : null}
</>
) : ( ) : (
parameters.map((param: NodeTypeParameter) => { parameters.map((param: NodeTypeParameter) => {
// Safety net: hidden params have no UI footprint at all — no row, // Safety net: hidden params have no UI footprint at all — no row,

View file

@ -1,7 +1,6 @@
import { FaFolder, FaFile, FaTrash } from 'react-icons/fa'; import { FaFolder, FaFile, FaTrash } from 'react-icons/fa';
import type { TreeNodeProvider, TreeNode, Ownership, ScopeValue, TreeBatchAction } from '../types'; import type { TreeNodeProvider, TreeNode, Ownership, ScopeValue, TreeBatchAction } from '../types';
import api from '../../../../api'; import api from '../../../../api';
import { getUserDataCache } from '../../../../utils/userCache';
interface FolderData { interface FolderData {
id: string; id: string;
@ -137,7 +136,7 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
if ((f.parentId ?? null) === null) out.add(f.id); if ((f.parentId ?? null) === null) out.add(f.id);
} }
const paginationParam = JSON.stringify({ filters: { folderId: null }, pageSize: 500 }); const paginationParam = JSON.stringify({ filters: { folderId: null }, pageSize: 500 });
const filesRes = await api.get('/api/files/list', { params: { pagination: paginationParam } }); const filesRes = await api.get('/api/files/list', { params: { pagination: paginationParam, owner } });
const data = filesRes.data; const data = filesRes.data;
const rawFiles: FileData[] = (data && typeof data === 'object' && 'items' in data) const rawFiles: FileData[] = (data && typeof data === 'object' && 'items' in data)
? (Array.isArray(data.items) ? data.items : []) ? (Array.isArray(data.items) ? data.items : [])
@ -193,7 +192,7 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
} }
const paginationParam = JSON.stringify({ filters, pageSize: 500 }); const paginationParam = JSON.stringify({ filters, pageSize: 500 });
const filesRes = await api.get('/api/files/list', { const filesRes = await api.get('/api/files/list', {
params: { pagination: paginationParam }, params: { pagination: paginationParam, owner },
}); });
const data = filesRes.data; const data = filesRes.data;
let rawFiles: FileData[] = []; let rawFiles: FileData[] = [];
@ -203,10 +202,6 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
rawFiles = data; rawFiles = data;
} }
let matched = rawFiles.filter((f) => (f.folderId ?? null) === apiParentId); 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)); const fileNodes = matched.map((f) => _mapFileToNode(f, ownership));
if (apiParentId === null) { if (apiParentId === null) {
for (const n of fileNodes) n.parentId = synthRootId; for (const n of fileNodes) n.parentId = synthRootId;

View file

@ -69,6 +69,7 @@ export interface PaginationParams {
filters?: Record<string, any>; filters?: Record<string, any>;
search?: string; search?: string;
viewKey?: string; viewKey?: string;
owner?: 'all' | 'me' | 'shared';
} }
// Files list hook // Files list hook
@ -150,6 +151,7 @@ export function useUserFiles() {
groupField: string; groupField: string;
groupDirection?: 'asc' | 'desc'; groupDirection?: 'asc' | 'desc';
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: string }>; groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: string }>;
owner?: 'all' | 'me' | 'shared';
}) => { }) => {
const levels = base.groupByLevels?.length const levels = base.groupByLevels?.length
? base.groupByLevels ? base.groupByLevels
@ -164,7 +166,11 @@ export function useUserFiles() {
if (base.sort?.length) (pObj as { sort: typeof base.sort }).sort = base.sort; if (base.sort?.length) (pObj as { sort: typeof base.sort }).sort = base.sort;
if (base.viewKey) (pObj as { viewKey: string }).viewKey = base.viewKey; if (base.viewKey) (pObj as { viewKey: string }).viewKey = base.viewKey;
const { data } = await api.get('/api/files/list', { const { data } = await api.get('/api/files/list', {
params: { mode: 'groupSummary', pagination: JSON.stringify(pObj) }, params: {
mode: 'groupSummary',
pagination: JSON.stringify(pObj),
...(base.owner ? { owner: base.owner } : {}),
},
}); });
return Array.isArray(data?.groups) ? data.groups : []; return Array.isArray(data?.groups) ? data.groups : [];
}, },
@ -192,7 +198,10 @@ export function useUserFiles() {
if (paginationParams.search) (pObj as { search: string }).search = paginationParams.search; if (paginationParams.search) (pObj as { search: string }).search = paginationParams.search;
if (paginationParams.viewKey) (pObj as { viewKey: string }).viewKey = paginationParams.viewKey; if (paginationParams.viewKey) (pObj as { viewKey: string }).viewKey = paginationParams.viewKey;
const { data } = await api.get('/api/files/list', { const { data } = await api.get('/api/files/list', {
params: { pagination: JSON.stringify(pObj) }, params: {
pagination: JSON.stringify(pObj),
...(paginationParams.owner ? { owner: paginationParams.owner } : {}),
},
}); });
if (data && typeof data === 'object' && 'items' in data) { if (data && typeof data === 'object' && 'items' in data) {
return { items: data.items, pagination: data.pagination }; return { items: data.items, pagination: data.pagination };

View file

@ -31,6 +31,17 @@ interface UserFile {
} }
type ViewMode = 'folder' | 'all'; type ViewMode = 'folder' | 'all';
type FileOwnerScope = 'all' | 'me' | 'shared';
function normalizeFolderFilterId(folderId: string | null): string | null {
if (!folderId) return null;
if (folderId.startsWith('__filesRoot:')) return null;
return folderId;
}
function isSyntheticRootFolderId(folderId: string | null): boolean {
return Boolean(folderId && folderId.startsWith('__filesRoot:'));
}
export const FilesPage: React.FC = () => { export const FilesPage: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
@ -74,6 +85,7 @@ export const FilesPage: React.FC = () => {
const [editingFile, setEditingFile] = useState<UserFile | null>(null); const [editingFile, setEditingFile] = useState<UserFile | null>(null);
const [selectedFiles, setSelectedFiles] = useState<UserFile[]>([]); const [selectedFiles, setSelectedFiles] = useState<UserFile[]>([]);
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null); const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
const [selectedOwnership, setSelectedOwnership] = useState<'own' | 'shared' | null>('own');
const [highlightedFileId, setHighlightedFileId] = useState<string | null>(null); const [highlightedFileId, setHighlightedFileId] = useState<string | null>(null);
const [treeWidth, setTreeWidth] = useState(300); const [treeWidth, setTreeWidth] = useState(300);
@ -103,14 +115,24 @@ export const FilesPage: React.FC = () => {
const _tableRefetch = useCallback(async (params?: any) => { const _tableRefetch = useCallback(async (params?: any) => {
const nextParams = { ...(params || {}) }; const nextParams = { ...(params || {}) };
const nextFilters = { ...(nextParams.filters || {}) }; const nextFilters = { ...(nextParams.filters || {}) };
if (viewMode === 'folder' && selectedFolderId) { const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
nextFilters.folderId = selectedFolderId; const rootSelected = isSyntheticRootFolderId(selectedFolderId);
const owner: FileOwnerScope =
selectedOwnership === 'own'
? 'me'
: selectedOwnership === 'shared'
? 'shared'
: 'all';
if (viewMode === 'folder' && selectedFolderId && !rootSelected) {
nextFilters.folderId = normalizedFolderId;
} else { } else {
delete nextFilters.folderId; delete nextFilters.folderId;
} }
nextParams.filters = nextFilters; nextParams.filters = nextFilters;
if (owner !== 'all') nextParams.owner = owner;
else delete nextParams.owner;
await tableRefetch(nextParams); await tableRefetch(nextParams);
}, [tableRefetch, selectedFolderId, viewMode]); }, [tableRefetch, selectedFolderId, selectedOwnership, viewMode]);
const fetchGroupSectionSummaries = useCallback( const fetchGroupSectionSummaries = useCallback(
async (base: { async (base: {
@ -122,12 +144,20 @@ export const FilesPage: React.FC = () => {
groupDirection?: 'asc' | 'desc'; groupDirection?: 'asc' | 'desc';
}) => { }) => {
const filters = { ...(base.filters || {}) }; const filters = { ...(base.filters || {}) };
if (viewMode === 'folder' && selectedFolderId) { const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
filters.folderId = selectedFolderId; const rootSelected = isSyntheticRootFolderId(selectedFolderId);
if (viewMode === 'folder' && selectedFolderId && !rootSelected) {
filters.folderId = normalizedFolderId;
} }
return fetchGroupSectionSummariesFromHook({ ...base, filters }); const owner: FileOwnerScope =
selectedOwnership === 'own'
? 'me'
: selectedOwnership === 'shared'
? 'shared'
: 'all';
return fetchGroupSectionSummariesFromHook({ ...base, filters, owner });
}, },
[fetchGroupSectionSummariesFromHook, viewMode, selectedFolderId], [fetchGroupSectionSummariesFromHook, viewMode, selectedFolderId, selectedOwnership],
); );
const refetchForSection = useCallback( const refetchForSection = useCallback(
@ -137,12 +167,20 @@ export const FilesPage: React.FC = () => {
parentColumnFilters?: Record<string, unknown>, parentColumnFilters?: Record<string, unknown>,
) => { ) => {
const merged = { ...(parentColumnFilters || {}) }; const merged = { ...(parentColumnFilters || {}) };
if (viewMode === 'folder' && selectedFolderId) { const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
merged.folderId = selectedFolderId; const rootSelected = isSyntheticRootFolderId(selectedFolderId);
if (viewMode === 'folder' && selectedFolderId && !rootSelected) {
merged.folderId = normalizedFolderId;
} }
return refetchForSectionFromHook(paginationParams, sectionFilter, merged); const owner: FileOwnerScope =
selectedOwnership === 'own'
? 'me'
: selectedOwnership === 'shared'
? 'shared'
: 'all';
return refetchForSectionFromHook({ ...paginationParams, owner }, sectionFilter, merged);
}, },
[refetchForSectionFromHook, viewMode, selectedFolderId], [refetchForSectionFromHook, viewMode, selectedFolderId, selectedOwnership],
); );
const _refreshAll = useCallback(async () => { const _refreshAll = useCallback(async () => {
@ -152,14 +190,15 @@ export const FilesPage: React.FC = () => {
useEffect(() => { useEffect(() => {
_tableRefetch({ page: 1, pageSize: 25 }); _tableRefetch({ page: 1, pageSize: 25 });
}, [selectedFolderId, viewMode, _tableRefetch]); }, [selectedFolderId, selectedOwnership, viewMode, _tableRefetch]);
// ── Tree interaction ────────────────────────────────────────────────── // ── Tree interaction ──────────────────────────────────────────────────
const _handleTreeNodeClick = useCallback((node: TreeNode) => { const _handleTreeNodeClick = useCallback((node: TreeNode) => {
setSelectedOwnership(node.ownership);
if (node.type === 'folder') { if (node.type === 'folder') {
setSelectedFolderId(node.id); setSelectedFolderId(node.id);
} else if (node.type === 'file') { } else if (node.type === 'file') {
setSelectedFolderId(node.parentId); setSelectedFolderId(node.parentId ?? null);
setHighlightedFileId(node.id); setHighlightedFileId(node.id);
requestAnimationFrame(() => { requestAnimationFrame(() => {
const row = document.querySelector('tr[data-highlighted="true"]'); const row = document.querySelector('tr[data-highlighted="true"]');