updated feature automation and feature chat playground routes

This commit is contained in:
Ida Dittrich 2026-03-09 13:49:03 +01:00
parent 0dbcad771b
commit df1344c228
5 changed files with 147 additions and 106 deletions

View file

@ -102,12 +102,15 @@ function App() {
<Route path="gdpr" element={<GDPRPage />} /> <Route path="gdpr" element={<GDPRPage />} />
{/* ============================================== */} {/* ============================================== */}
{/* WORKFLOWS ROUTES (global) */} {/* WORKFLOWS ROUTES (deprecated - redirect to /) */}
{/* Workflows are accessed via feature routes: */}
{/* /mandates/:mandateId/chatplayground/:id/workflows */}
{/* /mandates/:mandateId/automation/:id/definitions */}
{/* ============================================== */} {/* ============================================== */}
<Route path="workflows"> <Route path="workflows">
<Route path="playground" element={<PlaygroundPage />} /> <Route path="playground" element={<PlaygroundPage />} />
<Route path="list" element={<WorkflowsPage />} /> <Route path="list" element={<Navigate to="/" replace />} />
<Route path="automations" element={<AutomationDefinitionsView />} /> <Route path="automations" element={<Navigate to="/" replace />} />
</Route> </Route>
{/* ============================================== */} {/* ============================================== */}

View file

@ -69,17 +69,24 @@ export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<an
// ============================================================================ // ============================================================================
/** /**
* Fetch all workflows for the current user * Fetch workflows - feature-scoped or global (legacy)
* Endpoint: GET /api/workflows/ * When apiBaseUrl is provided: GET {apiBaseUrl}/workflows
* Otherwise (legacy): GET /api/workflows/
*/ */
export async function fetchWorkflows(request: ApiRequestFunction): Promise<Workflow[]> { export async function fetchWorkflows(
console.log('📤 fetchWorkflows: Making API request to /api/workflows/'); request: ApiRequestFunction,
params?: { pagination?: string },
apiBaseUrl?: string
): Promise<Workflow[] | { items: Workflow[]; pagination?: any }> {
const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
console.log('📤 fetchWorkflows: Making API request to', url);
try { try {
const data = await request({ const requestConfig: any = { url, method: 'get' as const };
url: '/api/workflows/', if (params?.pagination) {
method: 'get' requestConfig.params = { pagination: params.pagination };
}); }
const data = await request(requestConfig);
console.log('📥 fetchWorkflows: API response:', data); console.log('📥 fetchWorkflows: API response:', data);
@ -129,30 +136,30 @@ export async function fetchWorkflows(request: ApiRequestFunction): Promise<Workf
/** /**
* Fetch a single workflow by ID * Fetch a single workflow by ID
* Endpoint: GET /api/workflows/{workflowId} * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}
* Otherwise: GET /api/workflows/{workflowId}
*/ */
export async function fetchWorkflow( export async function fetchWorkflow(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string workflowId: string,
apiBaseUrl?: string
): Promise<Workflow & { messages?: WorkflowMessage[]; logs?: WorkflowLog[] }> { ): Promise<Workflow & { messages?: WorkflowMessage[]; logs?: WorkflowLog[] }> {
return await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
url: `/api/workflows/${workflowId}`, return await request({ url, method: 'get' });
method: 'get'
});
} }
/** /**
* Fetch workflow status (lightweight status check) * Fetch workflow status (lightweight status check)
* Endpoint: GET /api/workflows/{workflowId}/status * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/status
* Otherwise: GET /api/workflows/{workflowId}/status
*/ */
export async function fetchWorkflowStatus( export async function fetchWorkflowStatus(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string workflowId: string,
apiBaseUrl?: string
): Promise<Workflow | { status: string } | null> { ): Promise<Workflow | { status: string } | null> {
const data = await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/status` : `/api/workflows/${workflowId}/status`;
url: `/api/workflows/${workflowId}/status`, const data = await request({ url, method: 'get' });
method: 'get'
});
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
if (data.status) { if (data.status) {
@ -166,20 +173,18 @@ export async function fetchWorkflowStatus(
/** /**
* Fetch workflow messages * Fetch workflow messages
* Endpoint: GET /api/workflows/{workflowId}/messages * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/messages
* Query params: messageId (optional) - fetch only newer messages * Otherwise: GET /api/workflows/{workflowId}/messages
*/ */
export async function fetchWorkflowMessages( export async function fetchWorkflowMessages(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string, workflowId: string,
messageId?: string messageId?: string,
apiBaseUrl?: string
): Promise<WorkflowMessage[]> { ): Promise<WorkflowMessage[]> {
const params = messageId ? { messageId } : undefined; const params = messageId ? { messageId } : undefined;
const data = await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages` : `/api/workflows/${workflowId}/messages`;
url: `/api/workflows/${workflowId}/messages`, const data = await request({ url, method: 'get', params });
method: 'get',
params
});
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data; return data;
@ -199,20 +204,18 @@ export async function fetchWorkflowMessages(
/** /**
* Fetch workflow logs * Fetch workflow logs
* Endpoint: GET /api/workflows/{workflowId}/logs * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/logs
* Query params: logId (optional) - fetch only newer logs * Otherwise: GET /api/workflows/{workflowId}/logs
*/ */
export async function fetchWorkflowLogs( export async function fetchWorkflowLogs(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string, workflowId: string,
logId?: string logId?: string,
apiBaseUrl?: string
): Promise<WorkflowLog[]> { ): Promise<WorkflowLog[]> {
const params = logId ? { logId } : undefined; const params = logId ? { logId } : undefined;
const data = await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/logs` : `/api/workflows/${workflowId}/logs`;
url: `/api/workflows/${workflowId}/logs`, const data = await request({ url, method: 'get', params });
method: 'get',
params
});
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data; return data;
@ -232,7 +235,7 @@ export async function fetchWorkflowLogs(
/** /**
* Fetch unified chat data (messages, logs, stats, documents) * Fetch unified chat data (messages, logs, stats, documents)
* Endpoint: GET /api/chatplayground/{instanceId}/{workflowId}/chatData * Endpoint: GET /api/chatplayground/{instanceId}/workflows/{workflowId}/chatData
* Query params: afterTimestamp (optional) - fetch only data created after this time * Query params: afterTimestamp (optional) - fetch only data created after this time
*/ */
export async function fetchChatData( export async function fetchChatData(
@ -243,7 +246,7 @@ export async function fetchChatData(
): Promise<ChatDataResponse> { ): Promise<ChatDataResponse> {
const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined; const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined;
const requestConfig = { const requestConfig = {
url: `/api/chatplayground/${instanceId}/${workflowId}/chatData`, url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/chatData`,
method: 'get' as const, method: 'get' as const,
params params
}; };
@ -372,7 +375,7 @@ export async function startWorkflowApi(
/** /**
* Stop a running workflow * Stop a running workflow
* Endpoint: POST /api/chatplayground/{instanceId}/{workflowId}/stop * Endpoint: POST /api/chatplayground/{instanceId}/workflows/{workflowId}/stop
*/ */
export async function stopWorkflowApi( export async function stopWorkflowApi(
request: ApiRequestFunction, request: ApiRequestFunction,
@ -380,52 +383,53 @@ export async function stopWorkflowApi(
workflowId: string workflowId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/chatplayground/${instanceId}/${workflowId}/stop`, url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/stop`,
method: 'post' method: 'post'
}); });
} }
/** /**
* Update workflow properties * Update workflow properties
* Endpoint: PUT /api/workflows/{workflowId} * When apiBaseUrl provided: PUT {apiBaseUrl}/workflows/{workflowId}
* Otherwise: PUT /api/workflows/{workflowId}
*/ */
export async function updateWorkflowApi( export async function updateWorkflowApi(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string, workflowId: string,
updateData: Partial<{ name: string; description?: string; tags?: string[] }> updateData: Partial<{ name: string; description?: string; tags?: string[] }>,
apiBaseUrl?: string
): Promise<Workflow> { ): Promise<Workflow> {
return await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
url: `/api/workflows/${workflowId}`, return await request({ url, method: 'put', data: updateData });
method: 'put',
data: updateData
});
} }
/** /**
* Delete a workflow and all associated data * Delete a workflow and all associated data
* Endpoint: DELETE /api/workflows/{workflowId} * When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}
* Otherwise: DELETE /api/workflows/{workflowId}
*/ */
export async function deleteWorkflowApi( export async function deleteWorkflowApi(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string workflowId: string,
apiBaseUrl?: string
): Promise<void> { ): Promise<void> {
await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
url: `/api/workflows/${workflowId}`, await request({ url, method: 'delete' });
method: 'delete'
});
} }
/** /**
* Delete multiple workflows * Delete multiple workflows
* When apiBaseUrl provided, uses feature-scoped delete endpoint
*/ */
export async function deleteWorkflowsApi( export async function deleteWorkflowsApi(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowIds: string[] workflowIds: string[],
apiBaseUrl?: string
): Promise<void> { ): Promise<void> {
// Delete workflows one by one since there's no bulk delete endpoint const base = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows';
const deletePromises = workflowIds.map(workflowId => const deletePromises = workflowIds.map(workflowId =>
request({ request({
url: `/api/workflows/${workflowId}`, url: `${base}/${workflowId}`,
method: 'delete' method: 'delete'
}).catch(error => { }).catch(error => {
console.error(`Failed to delete workflow ${workflowId}:`, error); console.error(`Failed to delete workflow ${workflowId}:`, error);
@ -438,33 +442,33 @@ export async function deleteWorkflowsApi(
/** /**
* Delete a message from a workflow * Delete a message from a workflow
* Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId} * When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId}
* Otherwise: DELETE /api/workflows/{workflowId}/messages/{messageId}
*/ */
export async function deleteMessageApi( export async function deleteMessageApi(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string, workflowId: string,
messageId: string messageId: string,
apiBaseUrl?: string
): Promise<void> { ): Promise<void> {
await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}` : `/api/workflows/${workflowId}/messages/${messageId}`;
url: `/api/workflows/${workflowId}/messages/${messageId}`, await request({ url, method: 'delete' });
method: 'delete'
});
} }
/** /**
* Delete a file reference from a message * Delete a file reference from a message
* Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId} * When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId}/files/{fileId}
* Otherwise: DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId}
*/ */
export async function deleteFileFromMessageApi( export async function deleteFileFromMessageApi(
request: ApiRequestFunction, request: ApiRequestFunction,
workflowId: string, workflowId: string,
messageId: string, messageId: string,
fileId: string fileId: string,
apiBaseUrl?: string
): Promise<void> { ): Promise<void> {
await request({ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}/files/${fileId}` : `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`;
url: `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`, await request({ url, method: 'delete' });
method: 'delete'
});
} }
/** /**

View file

@ -46,8 +46,17 @@ export interface PaginationParams {
search?: string; search?: string;
} }
// Workflows list hook /** Get apiBaseUrl from instanceId and featureCode for feature-scoped workflow APIs */
export function useUserWorkflows() { export function getWorkflowApiBaseUrl(instanceId: string | undefined, featureCode: string | undefined): string | undefined {
if (!instanceId || !featureCode) return undefined;
if (featureCode === 'chatplayground') return `/api/chatplayground/${instanceId}`;
if (featureCode === 'automation') return `/api/automations/${instanceId}`;
return undefined;
}
// Workflows list hook - pass instanceId and featureCode when in feature context for feature-scoped API
export function useUserWorkflows(options?: { instanceId?: string; featureCode?: string }) {
const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode);
const [workflows, setWorkflows] = useState<UserWorkflow[]>([]); const [workflows, setWorkflows] = useState<UserWorkflow[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]); const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null); const [permissions, setPermissions] = useState<UserPermissions | null>(null);
@ -113,16 +122,11 @@ export function useUserWorkflows() {
} }
let data: any; let data: any;
const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
if (Object.keys(requestParams).length > 0) { if (Object.keys(requestParams).length > 0) {
// Use request for paginated queries data = await request({ url, method: 'get', params: requestParams });
data = await request({
url: '/api/workflows/',
method: 'get',
params: requestParams
});
} else { } else {
// Use API function for simple queries data = await fetchWorkflowsApi(request, undefined, apiBaseUrl);
data = await fetchWorkflowsApi(request);
} }
// Handle paginated response // Handle paginated response
@ -164,7 +168,7 @@ export function useUserWorkflows() {
setWorkflows([]); setWorkflows([]);
setPagination(null); setPagination(null);
} }
}, [request]); }, [request, apiBaseUrl]);
// Optimistically remove a workflow from the local state // Optimistically remove a workflow from the local state
const removeOptimistically = (workflowId: string) => { const removeOptimistically = (workflowId: string) => {
@ -185,13 +189,13 @@ export function useUserWorkflows() {
// Fetch a single workflow by ID // Fetch a single workflow by ID
const fetchWorkflowById = useCallback(async (workflowId: string): Promise<UserWorkflow | null> => { const fetchWorkflowById = useCallback(async (workflowId: string): Promise<UserWorkflow | null> => {
try { try {
const workflow = await fetchWorkflowByIdApi(request, workflowId); const workflow = await fetchWorkflowByIdApi(request, workflowId, apiBaseUrl);
return workflow as UserWorkflow | null; return workflow as UserWorkflow | null;
} catch (error: any) { } catch (error: any) {
console.error('Error fetching workflow by ID:', error); console.error('Error fetching workflow by ID:', error);
return null; return null;
} }
}, [request]); }, [request, apiBaseUrl]);
// Generate edit fields from attributes dynamically // Generate edit fields from attributes dynamically
const generateEditFieldsFromAttributes = useCallback((): Array<{ const generateEditFieldsFromAttributes = useCallback((): Array<{
@ -375,8 +379,9 @@ export function useUserWorkflows() {
}; };
} }
// Workflow operations hook // Workflow operations hook - pass instanceId and featureCode when in feature context for feature-scoped API
export function useWorkflowOperations() { export function useWorkflowOperations(options?: { instanceId?: string; featureCode?: string }) {
const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode);
const [startingWorkflow, setStartingWorkflow] = useState(false); const [startingWorkflow, setStartingWorkflow] = useState(false);
const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set()); const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set());
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set()); const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
@ -476,7 +481,7 @@ export function useWorkflowOperations() {
workflowId, workflowId,
setDeletingWorkflows, setDeletingWorkflows,
setDeleteError, setDeleteError,
() => deleteWorkflowApi(request, workflowId), () => deleteWorkflowApi(request, workflowId, apiBaseUrl),
{ {
default: 'Failed to delete workflow', default: 'Failed to delete workflow',
notFound: 'Workflow not found or has already been deleted.', notFound: 'Workflow not found or has already been deleted.',
@ -512,7 +517,7 @@ export function useWorkflowOperations() {
try { try {
// Delete workflows one by one since there's no bulk delete endpoint // Delete workflows one by one since there's no bulk delete endpoint
await deleteWorkflowsApi(request, workflowIds); await deleteWorkflowsApi(request, workflowIds, apiBaseUrl);
// Add a small delay to ensure backend has time to process // Add a small delay to ensure backend has time to process
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
@ -547,7 +552,7 @@ export function useWorkflowOperations() {
operationKey, operationKey,
setDeletingMessages, setDeletingMessages,
setDeleteMessageError, setDeleteMessageError,
() => deleteMessageApi(request, workflowId, messageId), () => deleteMessageApi(request, workflowId, messageId, apiBaseUrl),
{ {
default: 'Failed to delete message', default: 'Failed to delete message',
notFound: 'Message not found or has already been deleted.', notFound: 'Message not found or has already been deleted.',
@ -566,7 +571,7 @@ export function useWorkflowOperations() {
operationKey, operationKey,
setDeletingFiles, setDeletingFiles,
setDeleteFileError, setDeleteFileError,
() => deleteFileFromMessageApi(request, workflowId, messageId, fileId), () => deleteFileFromMessageApi(request, workflowId, messageId, fileId, apiBaseUrl),
{ {
default: 'Failed to delete file', default: 'Failed to delete file',
notFound: 'File not found or has already been deleted.', notFound: 'File not found or has already been deleted.',
@ -580,7 +585,7 @@ export function useWorkflowOperations() {
setEditingWorkflows(prev => new Set(prev).add(workflowId)); setEditingWorkflows(prev => new Set(prev).add(workflowId));
try { try {
const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData); const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData, apiBaseUrl);
return { success: true, workflowData: updatedWorkflow }; return { success: true, workflowData: updatedWorkflow };
} catch (error: any) { } catch (error: any) {

View file

@ -13,6 +13,7 @@ import { FaSync, FaRobot, FaRocket, FaPlus, FaFileAlt, FaStop, FaList, FaTimes,
import { useToast } from '../../../contexts/ToastContext'; import { useToast } from '../../../contexts/ToastContext';
import { useApiRequest } from '../../../hooks/useApi'; import { useApiRequest } from '../../../hooks/useApi';
import { useFeatureStore } from '../../../stores/featureStore'; import { useFeatureStore } from '../../../stores/featureStore';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import styles from '../../admin/Admin.module.css'; import styles from '../../admin/Admin.module.css';
interface WorkflowLog { interface WorkflowLog {
@ -24,11 +25,19 @@ interface WorkflowLog {
} }
export const AutomationDefinitionsView: React.FC = () => { export const AutomationDefinitionsView: React.FC = () => {
const { instanceId: routeInstanceId, featureCode: routeFeatureCode } = useCurrentInstance();
const { getAllInstances } = useFeatureStore(); const { getAllInstances } = useFeatureStore();
const instances = getAllInstances(); const instances = getAllInstances();
const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0]; const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0];
const mandateId = chatbotInstance?.mandateId; const automationInstance = instances.find(i => i.featureCode === 'automation');
const featureInstanceId = chatbotInstance?.id; // When under automation feature route, use route context; otherwise use featureStore
const mandateId = routeFeatureCode === 'automation' && routeInstanceId
? (automationInstance?.mandateId ?? chatbotInstance?.mandateId)
: chatbotInstance?.mandateId;
const featureInstanceId = routeFeatureCode === 'automation' && routeInstanceId
? routeInstanceId
: (chatbotInstance?.id ?? automationInstance?.id);
const automationWorkflowInstanceId = routeFeatureCode === 'automation' ? routeInstanceId : undefined;
const { const {
data: automations, data: automations,
@ -66,6 +75,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: boolean; visible: boolean;
automationId: string | null; automationId: string | null;
automationLabel: string; automationLabel: string;
featureInstanceId: string | null;
workflowId: string | null; workflowId: string | null;
status: 'starting' | 'running' | 'completed' | 'stopped' | 'error'; status: 'starting' | 'running' | 'completed' | 'stopped' | 'error';
logs: WorkflowLog[]; logs: WorkflowLog[];
@ -73,6 +83,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: false, visible: false,
automationId: null, automationId: null,
automationLabel: '', automationLabel: '',
featureInstanceId: null,
workflowId: null, workflowId: null,
status: 'starting', status: 'starting',
logs: [], logs: [],
@ -259,14 +270,16 @@ export const AutomationDefinitionsView: React.FC = () => {
setShowEditor(true); setShowEditor(true);
}; };
const pollWorkflowLogs = useCallback(async (workflowId: string) => { const pollWorkflowLogs = useCallback(async (workflowId: string, instanceId: string) => {
try { try {
const contextHeaders: Record<string, string> = {}; const contextHeaders: Record<string, string> = {};
if (mandateId) contextHeaders['X-Mandate-Id'] = mandateId; if (mandateId) contextHeaders['X-Mandate-Id'] = mandateId;
const logsUrl = `/api/automations/${instanceId}/workflows/${workflowId}/logs`;
const workflowUrl = `/api/automations/${instanceId}/workflows/${workflowId}`;
const response = await request({ const response = await request({
url: `/api/workflows/${workflowId}/logs`, url: logsUrl,
method: 'get', method: 'get',
params: lastLogIdRef.current ? { afterId: lastLogIdRef.current } : {}, params: lastLogIdRef.current ? { logId: lastLogIdRef.current } : {},
additionalConfig: { headers: contextHeaders }, additionalConfig: { headers: contextHeaders },
}); });
const logs: WorkflowLog[] = response?.items || response || []; const logs: WorkflowLog[] = response?.items || response || [];
@ -279,7 +292,7 @@ export const AutomationDefinitionsView: React.FC = () => {
lastLogIdRef.current = logs[logs.length - 1].id; lastLogIdRef.current = logs[logs.length - 1].id;
} }
const statusResponse = await request({ const statusResponse = await request({
url: `/api/workflows/${workflowId}`, url: workflowUrl,
method: 'get', method: 'get',
additionalConfig: { headers: contextHeaders }, additionalConfig: { headers: contextHeaders },
}); });
@ -309,6 +322,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: true, visible: true,
automationId: automation.id, automationId: automation.id,
automationLabel: automation.label, automationLabel: automation.label,
featureInstanceId: automation.featureInstanceId ?? automationWorkflowInstanceId ?? null,
workflowId: null, workflowId: null,
status: 'starting', status: 'starting',
logs: [{ id: 'init', timestamp: Date.now() / 1000, message: 'Automatisierung wird gestartet...' }], logs: [{ id: 'init', timestamp: Date.now() / 1000, message: 'Automatisierung wird gestartet...' }],
@ -316,14 +330,17 @@ export const AutomationDefinitionsView: React.FC = () => {
try { try {
const result = await handleAutomationExecute(automation.id); const result = await handleAutomationExecute(automation.id);
const workflowId = result?.id; const workflowId = result?.id;
if (workflowId) { const instanceId = automation.featureInstanceId ?? automationWorkflowInstanceId;
if (workflowId && instanceId) {
setExecutionModal(prev => ({ setExecutionModal(prev => ({
...prev, ...prev,
workflowId, workflowId,
status: 'running', status: 'running',
logs: [...prev.logs, { id: 'started', timestamp: Date.now() / 1000, message: `Workflow ${workflowId} gestartet`, status: 'running' }], logs: [...prev.logs, { id: 'started', timestamp: Date.now() / 1000, message: `Workflow ${workflowId} gestartet`, status: 'running' }],
})); }));
pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId), 2000); pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId, instanceId), 2000);
} else if (workflowId && !instanceId) {
setExecutionModal(prev => ({ ...prev, status: 'error', logs: [...prev.logs, { id: 'error', timestamp: Date.now() / 1000, message: 'Keine Feature-Instanz für Polling', status: 'error' }] }));
} }
} catch (err: any) { } catch (err: any) {
setExecutionModal(prev => ({ setExecutionModal(prev => ({
@ -337,11 +354,17 @@ export const AutomationDefinitionsView: React.FC = () => {
const handleStopWorkflow = async () => { const handleStopWorkflow = async () => {
if (!executionModal.workflowId) return; if (!executionModal.workflowId) return;
const instanceId = executionModal.featureInstanceId ?? automationWorkflowInstanceId;
if (!instanceId) {
showError('Keine Feature-Instanz für Stopp verfügbar');
return;
}
try { try {
const stopHeaders: Record<string, string> = {}; const stopHeaders: Record<string, string> = {};
if (mandateId) stopHeaders['X-Mandate-Id'] = mandateId; if (mandateId) stopHeaders['X-Mandate-Id'] = mandateId;
const stopUrl = `/api/automations/${instanceId}/workflows/${executionModal.workflowId}/stop`;
await request({ await request({
url: `/api/workflows/${executionModal.workflowId}/stop`, url: stopUrl,
method: 'post', method: 'post',
additionalConfig: { headers: stopHeaders }, additionalConfig: { headers: stopHeaders },
}); });
@ -363,6 +386,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: false, visible: false,
automationId: null, automationId: null,
automationLabel: '', automationLabel: '',
featureInstanceId: null,
workflowId: null, workflowId: null,
status: 'starting', status: 'starting',
logs: [], logs: [],

View file

@ -6,11 +6,12 @@
*/ */
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { useUserWorkflows, useWorkflowOperations } from '../../hooks/useWorkflows'; import { useUserWorkflows, useWorkflowOperations, getWorkflowApiBaseUrl } from '../../hooks/useWorkflows';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaList, FaPlay } from 'react-icons/fa'; import { FaSync, FaList, FaPlay } from 'react-icons/fa';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useCurrentInstance } from '../../hooks/useCurrentInstance';
import styles from '../admin/Admin.module.css'; import styles from '../admin/Admin.module.css';
interface Workflow { interface Workflow {
@ -23,8 +24,12 @@ interface Workflow {
export const WorkflowsPage: React.FC = () => { export const WorkflowsPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { instanceId, featureCode } = useCurrentInstance();
const workflowOptions = instanceId && featureCode ? { instanceId, featureCode } : undefined;
const apiBaseUrl = getWorkflowApiBaseUrl(instanceId, featureCode);
const apiEndpoint = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
// Data hook // Data hook - pass instance context when in feature route
const { const {
data: workflows, data: workflows,
attributes, attributes,
@ -35,16 +40,16 @@ export const WorkflowsPage: React.FC = () => {
refetch, refetch,
fetchWorkflowById, fetchWorkflowById,
updateOptimistically, updateOptimistically,
} = useUserWorkflows(); } = useUserWorkflows(workflowOptions);
// Operations hook // Operations hook - pass instance context when in feature route
const { const {
handleWorkflowDelete, handleWorkflowDelete,
handleWorkflowDeleteMultiple, handleWorkflowDeleteMultiple,
handleWorkflowUpdate, handleWorkflowUpdate,
handleInlineUpdate, handleInlineUpdate,
deletingWorkflows, deletingWorkflows,
} = useWorkflowOperations(); } = useWorkflowOperations(workflowOptions);
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null); const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
@ -173,7 +178,7 @@ export const WorkflowsPage: React.FC = () => {
<FormGeneratorTable <FormGeneratorTable
data={workflows} data={workflows}
columns={columns} columns={columns}
apiEndpoint="/api/workflows/" apiEndpoint={apiEndpoint}
loading={loading} loading={loading}
pagination={true} pagination={true}
pageSize={25} pageSize={25}