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;
viewKey?: string;
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
owner?: 'all' | 'me' | 'shared';
}
export interface PaginatedResponse<T> {
@ -109,6 +110,7 @@ export async function fetchFiles(
if (params.search) paginationObj.search = params.search;
if (params.viewKey) paginationObj.viewKey = params.viewKey;
if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
if (params.owner) requestParams.owner = params.owner;
if (Object.keys(paginationObj).length > 0) {
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 { getLabel } from '../nodes/shared/utils';
import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers';
import { ContextBuilderRenderer } from '../nodes/frontendTypeRenderers/ContextBuilderRenderer';
import { RequiredAttributePicker } from '../nodes/shared/RequiredAttributePicker';
import { findRequiredErrors } from '../nodes/shared/paramValidation';
import { useAutomation2DataFlow } from '../context/Automation2DataFlowContext';
@ -253,6 +254,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
for (const param of sortedParameters) {
if (param.frontendType === 'hidden') continue;
if (param.name === 'context') continue;
if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue;
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue;
@ -378,6 +380,15 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
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;
const isTrigger = node.type.startsWith('trigger.');
@ -483,11 +494,71 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
</div>
)}
{extractContentAccordionItems !== null ? (
<AccordionList<string>
key={`${node.id}-extract-accordion`}
defaultOpenId={null}
items={extractContentAccordionItems}
/>
<>
{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>
key={`${node.id}-extract-accordion`}
defaultOpenId={null}
items={extractContentAccordionItems}
/>
) : null}
</>
) : (
parameters.map((param: NodeTypeParameter) => {
// 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 type { TreeNodeProvider, TreeNode, Ownership, ScopeValue, TreeBatchAction } from '../types';
import api from '../../../../api';
import { getUserDataCache } from '../../../../utils/userCache';
interface FolderData {
id: string;
@ -137,7 +136,7 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
if ((f.parentId ?? null) === null) out.add(f.id);
}
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 rawFiles: FileData[] = (data && typeof data === 'object' && 'items' in data)
? (Array.isArray(data.items) ? data.items : [])
@ -193,7 +192,7 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
}
const paginationParam = JSON.stringify({ filters, pageSize: 500 });
const filesRes = await api.get('/api/files/list', {
params: { pagination: paginationParam },
params: { pagination: paginationParam, owner },
});
const data = filesRes.data;
let rawFiles: FileData[] = [];
@ -203,10 +202,6 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
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;

View file

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

View file

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