automation fixautomation fixeses
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 56s
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 56s
This commit is contained in:
parent
7eb305f910
commit
6520763736
11 changed files with 112 additions and 36 deletions
|
|
@ -531,17 +531,18 @@ export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<an
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch node types for the flow builder (backend-driven).
|
* Fetch node types for the flow builder (backend-driven).
|
||||||
* GET /api/workflow-automation/node-types?language=de
|
* GET /api/workflow-automation/node-types?mandateId=...&language=de
|
||||||
*/
|
*/
|
||||||
export async function fetchNodeTypes(
|
export async function fetchNodeTypes(
|
||||||
request: ApiRequestFunction,
|
request: ApiRequestFunction,
|
||||||
|
mandateId: string,
|
||||||
language = 'de'
|
language = 'de'
|
||||||
): Promise<NodeTypesResponse> {
|
): Promise<NodeTypesResponse> {
|
||||||
console.log(`${LOG} fetchNodeTypes: language=${language}`);
|
console.log(`${LOG} fetchNodeTypes: mandateId=${mandateId} language=${language}`);
|
||||||
const data = await request({
|
const data = await request({
|
||||||
url: `${BASE}/node-types`,
|
url: `${BASE}/node-types`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { language },
|
params: { mandateId, language },
|
||||||
});
|
});
|
||||||
const nodeTypes = data?.nodeTypes ?? [];
|
const nodeTypes = data?.nodeTypes ?? [];
|
||||||
const categories = data?.categories ?? [];
|
const categories = data?.categories ?? [];
|
||||||
|
|
@ -717,6 +718,7 @@ export async function fetchWorkflows(
|
||||||
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
||||||
});
|
});
|
||||||
if (data?.items && data?.pagination) return data;
|
if (data?.items && data?.pagination) return data;
|
||||||
|
if (data?.items) return data.items;
|
||||||
return data?.workflows ?? [];
|
return data?.workflows ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -990,7 +992,7 @@ export async function fetchTasks(
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
return data?.tasks ?? [];
|
return data?.items ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function completeTask(
|
export async function completeTask(
|
||||||
|
|
|
||||||
|
|
@ -551,7 +551,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data = await fetchNodeTypes(request, language);
|
const data = await fetchNodeTypes(request, mandateId || '', language);
|
||||||
setNodeTypes(data.nodeTypes);
|
setNodeTypes(data.nodeTypes);
|
||||||
setCategories(data.categories);
|
setCategories(data.categories);
|
||||||
if (data.portTypeCatalog) {
|
if (data.portTypeCatalog) {
|
||||||
|
|
@ -572,12 +572,12 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
|
|
||||||
const loadWorkflows = useCallback(async () => {
|
const loadWorkflows = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await fetchWorkflows(request);
|
const result = await fetchWorkflows(request, { mandateId: mandateId || undefined });
|
||||||
setWorkflows(Array.isArray(result) ? result : result.items);
|
setWorkflows(Array.isArray(result) ? result : result.items);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`${LOG} loadWorkflows failed`, e);
|
console.error(`${LOG} loadWorkflows failed`, e);
|
||||||
}
|
}
|
||||||
}, [request]);
|
}, [request, mandateId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadNodeTypes();
|
loadNodeTypes();
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,78 @@ export function fromApiGraph(
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasPositions = nodes.length > 0 && nodes.some((n) => n.x !== 0 || n.y !== 0);
|
||||||
|
if (!hasPositions && nodes.length > 1) {
|
||||||
|
_autoLayoutTopDown(nodes, connections);
|
||||||
|
}
|
||||||
|
|
||||||
return { nodes, connections };
|
return { nodes, connections };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _NODE_WIDTH = 200;
|
||||||
|
const _NODE_HEIGHT = 60;
|
||||||
|
const _VERTICAL_GAP = 80;
|
||||||
|
const _HORIZONTAL_GAP = 60;
|
||||||
|
|
||||||
|
function _autoLayoutTopDown(nodes: CanvasNode[], connections: CanvasConnection[]): void {
|
||||||
|
const inDegree = new Map<string, number>();
|
||||||
|
const children = new Map<string, string[]>();
|
||||||
|
for (const n of nodes) {
|
||||||
|
inDegree.set(n.id, 0);
|
||||||
|
children.set(n.id, []);
|
||||||
|
}
|
||||||
|
for (const c of connections) {
|
||||||
|
inDegree.set(c.targetId, (inDegree.get(c.targetId) ?? 0) + 1);
|
||||||
|
children.get(c.sourceId)?.push(c.targetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layers: string[][] = [];
|
||||||
|
const assigned = new Set<string>();
|
||||||
|
const queue = nodes.filter((n) => (inDegree.get(n.id) ?? 0) === 0).map((n) => n.id);
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const layer = [...queue];
|
||||||
|
layers.push(layer);
|
||||||
|
layer.forEach((id) => assigned.add(id));
|
||||||
|
const next: string[] = [];
|
||||||
|
for (const id of layer) {
|
||||||
|
for (const child of children.get(id) || []) {
|
||||||
|
if (assigned.has(child)) continue;
|
||||||
|
const remaining = connections.filter(
|
||||||
|
(c) => c.targetId === child && !assigned.has(c.sourceId)
|
||||||
|
);
|
||||||
|
if (remaining.length === 0) next.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue.length = 0;
|
||||||
|
const unique = [...new Set(next)];
|
||||||
|
queue.push(...unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (!assigned.has(n.id)) {
|
||||||
|
layers.push([n.id]);
|
||||||
|
assigned.add(n.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
||||||
|
let y = 40;
|
||||||
|
for (const layer of layers) {
|
||||||
|
const totalWidth = layer.length * _NODE_WIDTH + (layer.length - 1) * _HORIZONTAL_GAP;
|
||||||
|
let x = Math.max(40, (600 - totalWidth) / 2);
|
||||||
|
for (const id of layer) {
|
||||||
|
const node = nodeById.get(id);
|
||||||
|
if (node) {
|
||||||
|
node.x = x;
|
||||||
|
node.y = y;
|
||||||
|
x += _NODE_WIDTH + _HORIZONTAL_GAP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y += _NODE_HEIGHT + _VERTICAL_GAP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function toApiGraph(
|
export function toApiGraph(
|
||||||
nodes: CanvasNode[],
|
nodes: CanvasNode[],
|
||||||
connections: CanvasConnection[]
|
connections: CanvasConnection[]
|
||||||
|
|
|
||||||
|
|
@ -913,13 +913,14 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
return columnsRef.current;
|
return columnsRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NO COLUMNS PROVIDED - this is an error in the calling component
|
// Only warn when data is present but columns are still missing
|
||||||
// The calling component should provide columns from the /attributes/{entityType} endpoint
|
if (data && data.length > 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'⚠️ FormGeneratorTable: No columns provided! ' +
|
'⚠️ FormGeneratorTable: No columns provided! ' +
|
||||||
'Columns should come from Pydantic attribute definitions via /attributes/{entityType} endpoint. ' +
|
'Columns should come from Pydantic attribute definitions via /attributes/{entityType} endpoint. ' +
|
||||||
'Please ensure the calling component fetches and passes columns from the backend.'
|
'Please ensure the calling component fetches and passes columns from the backend.'
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Return empty array - table will show no columns
|
// Return empty array - table will show no columns
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock,
|
FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock,
|
||||||
FaHeadset, FaVideo, FaHatWizard, FaStore, FaUserTie, FaClipboardList,
|
FaHeadset, FaVideo, FaHatWizard, FaStore, FaUserTie, FaClipboardList,
|
||||||
FaFileContract, FaGlobe, FaClipboardCheck,
|
FaFileContract, FaGlobe, FaClipboardCheck,
|
||||||
FaSitemap, FaCopy, FaTasks,
|
FaSitemap,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -58,11 +58,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
||||||
'page.system.ragInventory': <FaDatabase />,
|
'page.system.ragInventory': <FaDatabase />,
|
||||||
|
|
||||||
// System pages - Workflow Automation
|
// System pages - Workflow Automation
|
||||||
'page.system.workflowAutomation.workflows': <FaSitemap />,
|
'page.system.workflowAutomation': <FaSitemap />,
|
||||||
'page.system.workflowAutomation.editor': <FaProjectDiagram />,
|
|
||||||
'page.system.workflowAutomation.templates': <FaCopy />,
|
|
||||||
'page.system.workflowAutomation.runs': <FaPlay />,
|
|
||||||
'page.system.workflowAutomation.tasks': <FaTasks />,
|
|
||||||
|
|
||||||
// Billing pages (legacy compat)
|
// Billing pages (legacy compat)
|
||||||
'page.billing.dashboard': <FaWallet />,
|
'page.billing.dashboard': <FaWallet />,
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,11 @@ export function useUserFiles() {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching permissions:', error);
|
console.error('Error fetching permissions:', error);
|
||||||
const defaultPerms: UserPermissions = {
|
const defaultPerms: UserPermissions = {
|
||||||
view: false,
|
view: true,
|
||||||
read: 'n',
|
read: 'my',
|
||||||
create: 'n',
|
create: 'my',
|
||||||
update: 'n',
|
update: 'my',
|
||||||
delete: 'n',
|
delete: 'my',
|
||||||
};
|
};
|
||||||
setPermissions(defaultPerms);
|
setPermissions(defaultPerms);
|
||||||
return defaultPerms;
|
return defaultPerms;
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,12 @@ async function startWorkflowApi(
|
||||||
workflowData: StartWorkflowRequest,
|
workflowData: StartWorkflowRequest,
|
||||||
options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' },
|
options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' },
|
||||||
) {
|
) {
|
||||||
|
const wfId = options?.workflowId ?? (workflowData as { workflowId?: string }).workflowId;
|
||||||
return await request({
|
return await request({
|
||||||
url: `/api/workflow-automation/execute`,
|
url: `/api/workflow-automation/execute`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
workflowId: options?.workflowId ?? (workflowData as { workflowId?: string }).workflowId,
|
workflowId: wfId,
|
||||||
payload: workflowData,
|
payload: workflowData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -575,7 +575,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
label: t('Datei'),
|
label: t('Datei'),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
width: 140,
|
width: 140,
|
||||||
formatter: (val: any) => (val ? `${String(val).slice(0, 8)}…` : '–'),
|
displayField: 'fileIdLabel',
|
||||||
},
|
},
|
||||||
], [t]);
|
], [t]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@ export const WorkflowAutomationPage: React.FC = () => {
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<string>(initialRunId ? 'detail' : initialTab);
|
const [activeTab, setActiveTab] = useState<string>(initialRunId ? 'detail' : initialTab);
|
||||||
const [selectedRunId, setSelectedRunId] = useState<string | null>(initialRunId);
|
const [selectedRunId, setSelectedRunId] = useState<string | null>(initialRunId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newTab = _TAB_ALIASES[searchParams.get('tab') || 'workflows'] || searchParams.get('tab') || 'workflows';
|
||||||
|
if (newTab !== activeTab) {
|
||||||
|
setActiveTab(newTab);
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
const [workflowFilter, setWorkflowFilter] = useState<string | null>(null);
|
const [workflowFilter, setWorkflowFilter] = useState<string | null>(null);
|
||||||
const [selectedMandateId, setSelectedMandateId] = useState<string>('all');
|
const [selectedMandateId, setSelectedMandateId] = useState<string>('all');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
|
||||||
if (selectedMandateId !== 'all') params.mandateId = selectedMandateId;
|
if (selectedMandateId !== 'all') params.mandateId = selectedMandateId;
|
||||||
const resp = await api.get('/api/workflow-automation/runs', { params });
|
const resp = await api.get('/api/workflow-automation/runs', { params });
|
||||||
const data = resp.data;
|
const data = resp.data;
|
||||||
setRuns(data?.runs || []);
|
setRuns(data?.items || []);
|
||||||
const total = data?.total ?? 0;
|
const total = data?.total ?? 0;
|
||||||
const pageSize = pag.pageSize;
|
const pageSize = pag.pageSize;
|
||||||
setPaginationMeta({
|
setPaginationMeta({
|
||||||
|
|
@ -416,7 +416,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
|
||||||
width: 140,
|
width: 140,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
displayField: 'mandateLabel',
|
displayField: 'mandateIdLabel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'featureInstanceId',
|
key: 'featureInstanceId',
|
||||||
|
|
@ -424,7 +424,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
|
||||||
width: 140,
|
width: 140,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
displayField: 'instanceLabel',
|
displayField: 'featureInstanceIdLabel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ownerId',
|
key: 'ownerId',
|
||||||
|
|
@ -432,7 +432,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
|
||||||
width: 140,
|
width: 140,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
displayField: 'ownerLabel',
|
displayField: 'ownerIdLabel',
|
||||||
},
|
},
|
||||||
{ key: 'status', width: 110, sortable: true, filterable: true },
|
{ key: 'status', width: 110, sortable: true, filterable: true },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ export const _WorkflowsTab: React.FC<WorkflowsTabProps> = ({ onWorkflowClick, se
|
||||||
width: 140,
|
width: 140,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
displayField: 'mandateLabel',
|
displayField: 'mandateIdLabel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'featureInstanceId',
|
key: 'featureInstanceId',
|
||||||
|
|
@ -228,15 +228,15 @@ export const _WorkflowsTab: React.FC<WorkflowsTabProps> = ({ onWorkflowClick, se
|
||||||
width: 140,
|
width: 140,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
displayField: 'instanceLabel',
|
displayField: 'featureInstanceIdLabel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ownerId',
|
key: 'sysCreatedBy',
|
||||||
label: t('Benutzer'),
|
label: t('Ersteller'),
|
||||||
width: 140,
|
width: 140,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
displayField: 'ownerLabel',
|
displayField: 'sysCreatedByLabel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'active',
|
key: 'active',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue