diff --git a/src/App.tsx b/src/App.tsx
index 7a058cd..d56bc0a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -102,12 +102,15 @@ function App() {
} />
{/* ============================================== */}
- {/* WORKFLOWS ROUTES (global) */}
+ {/* WORKFLOWS ROUTES (deprecated - redirect to /) */}
+ {/* Workflows are accessed via feature routes: */}
+ {/* /mandates/:mandateId/chatplayground/:id/workflows */}
+ {/* /mandates/:mandateId/automation/:id/definitions */}
{/* ============================================== */}
} />
- } />
- } />
+ } />
+ } />
{/* ============================================== */}
diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts
index a6191b4..3d7b0db 100644
--- a/src/api/workflowApi.ts
+++ b/src/api/workflowApi.ts
@@ -69,17 +69,24 @@ export type ApiRequestFunction = (options: ApiRequestOptions) => Promise {
- console.log('📤 fetchWorkflows: Making API request to /api/workflows/');
+export async function fetchWorkflows(
+ request: ApiRequestFunction,
+ params?: { pagination?: string },
+ apiBaseUrl?: string
+): Promise {
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
+ console.log('📤 fetchWorkflows: Making API request to', url);
try {
- const data = await request({
- url: '/api/workflows/',
- method: 'get'
- });
+ const requestConfig: any = { url, method: 'get' as const };
+ if (params?.pagination) {
+ requestConfig.params = { pagination: params.pagination };
+ }
+ const data = await request(requestConfig);
console.log('📥 fetchWorkflows: API response:', data);
@@ -129,30 +136,30 @@ export async function fetchWorkflows(request: ApiRequestFunction): Promise {
- return await request({
- url: `/api/workflows/${workflowId}`,
- method: 'get'
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
+ return await request({ url, method: 'get' });
}
/**
* 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(
request: ApiRequestFunction,
- workflowId: string
+ workflowId: string,
+ apiBaseUrl?: string
): Promise {
- const data = await request({
- url: `/api/workflows/${workflowId}/status`,
- method: 'get'
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/status` : `/api/workflows/${workflowId}/status`;
+ const data = await request({ url, method: 'get' });
if (data && typeof data === 'object') {
if (data.status) {
@@ -166,20 +173,18 @@ export async function fetchWorkflowStatus(
/**
* Fetch workflow messages
- * Endpoint: GET /api/workflows/{workflowId}/messages
- * Query params: messageId (optional) - fetch only newer messages
+ * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/messages
+ * Otherwise: GET /api/workflows/{workflowId}/messages
*/
export async function fetchWorkflowMessages(
request: ApiRequestFunction,
workflowId: string,
- messageId?: string
+ messageId?: string,
+ apiBaseUrl?: string
): Promise {
const params = messageId ? { messageId } : undefined;
- const data = await request({
- url: `/api/workflows/${workflowId}/messages`,
- method: 'get',
- params
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages` : `/api/workflows/${workflowId}/messages`;
+ const data = await request({ url, method: 'get', params });
if (Array.isArray(data)) {
return data;
@@ -199,20 +204,18 @@ export async function fetchWorkflowMessages(
/**
* Fetch workflow logs
- * Endpoint: GET /api/workflows/{workflowId}/logs
- * Query params: logId (optional) - fetch only newer logs
+ * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/logs
+ * Otherwise: GET /api/workflows/{workflowId}/logs
*/
export async function fetchWorkflowLogs(
request: ApiRequestFunction,
workflowId: string,
- logId?: string
+ logId?: string,
+ apiBaseUrl?: string
): Promise {
const params = logId ? { logId } : undefined;
- const data = await request({
- url: `/api/workflows/${workflowId}/logs`,
- method: 'get',
- params
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/logs` : `/api/workflows/${workflowId}/logs`;
+ const data = await request({ url, method: 'get', params });
if (Array.isArray(data)) {
return data;
@@ -232,7 +235,7 @@ export async function fetchWorkflowLogs(
/**
* 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
*/
export async function fetchChatData(
@@ -243,7 +246,7 @@ export async function fetchChatData(
): Promise {
const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined;
const requestConfig = {
- url: `/api/chatplayground/${instanceId}/${workflowId}/chatData`,
+ url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/chatData`,
method: 'get' as const,
params
};
@@ -372,7 +375,7 @@ export async function startWorkflowApi(
/**
* Stop a running workflow
- * Endpoint: POST /api/chatplayground/{instanceId}/{workflowId}/stop
+ * Endpoint: POST /api/chatplayground/{instanceId}/workflows/{workflowId}/stop
*/
export async function stopWorkflowApi(
request: ApiRequestFunction,
@@ -380,52 +383,53 @@ export async function stopWorkflowApi(
workflowId: string
): Promise {
await request({
- url: `/api/chatplayground/${instanceId}/${workflowId}/stop`,
+ url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/stop`,
method: 'post'
});
}
/**
* Update workflow properties
- * Endpoint: PUT /api/workflows/{workflowId}
+ * When apiBaseUrl provided: PUT {apiBaseUrl}/workflows/{workflowId}
+ * Otherwise: PUT /api/workflows/{workflowId}
*/
export async function updateWorkflowApi(
request: ApiRequestFunction,
workflowId: string,
- updateData: Partial<{ name: string; description?: string; tags?: string[] }>
+ updateData: Partial<{ name: string; description?: string; tags?: string[] }>,
+ apiBaseUrl?: string
): Promise {
- return await request({
- url: `/api/workflows/${workflowId}`,
- method: 'put',
- data: updateData
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
+ return await request({ url, method: 'put', data: updateData });
}
/**
* 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(
request: ApiRequestFunction,
- workflowId: string
+ workflowId: string,
+ apiBaseUrl?: string
): Promise {
- await request({
- url: `/api/workflows/${workflowId}`,
- method: 'delete'
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
+ await request({ url, method: 'delete' });
}
/**
* Delete multiple workflows
+ * When apiBaseUrl provided, uses feature-scoped delete endpoint
*/
export async function deleteWorkflowsApi(
request: ApiRequestFunction,
- workflowIds: string[]
+ workflowIds: string[],
+ apiBaseUrl?: string
): Promise {
- // Delete workflows one by one since there's no bulk delete endpoint
+ const base = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows';
const deletePromises = workflowIds.map(workflowId =>
request({
- url: `/api/workflows/${workflowId}`,
+ url: `${base}/${workflowId}`,
method: 'delete'
}).catch(error => {
console.error(`Failed to delete workflow ${workflowId}:`, error);
@@ -438,33 +442,33 @@ export async function deleteWorkflowsApi(
/**
* 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(
request: ApiRequestFunction,
workflowId: string,
- messageId: string
+ messageId: string,
+ apiBaseUrl?: string
): Promise {
- await request({
- url: `/api/workflows/${workflowId}/messages/${messageId}`,
- method: 'delete'
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}` : `/api/workflows/${workflowId}/messages/${messageId}`;
+ await request({ url, method: 'delete' });
}
/**
* 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(
request: ApiRequestFunction,
workflowId: string,
messageId: string,
- fileId: string
+ fileId: string,
+ apiBaseUrl?: string
): Promise {
- await request({
- url: `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`,
- method: 'delete'
- });
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}/files/${fileId}` : `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`;
+ await request({ url, method: 'delete' });
}
/**
diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts
index 79c12cc..adcb033 100644
--- a/src/hooks/useWorkflows.ts
+++ b/src/hooks/useWorkflows.ts
@@ -46,8 +46,17 @@ export interface PaginationParams {
search?: string;
}
-// Workflows list hook
-export function useUserWorkflows() {
+/** Get apiBaseUrl from instanceId and featureCode for feature-scoped workflow APIs */
+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([]);
const [attributes, setAttributes] = useState([]);
const [permissions, setPermissions] = useState(null);
@@ -113,16 +122,11 @@ export function useUserWorkflows() {
}
let data: any;
+ const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
if (Object.keys(requestParams).length > 0) {
- // Use request for paginated queries
- data = await request({
- url: '/api/workflows/',
- method: 'get',
- params: requestParams
- });
+ data = await request({ url, method: 'get', params: requestParams });
} else {
- // Use API function for simple queries
- data = await fetchWorkflowsApi(request);
+ data = await fetchWorkflowsApi(request, undefined, apiBaseUrl);
}
// Handle paginated response
@@ -164,7 +168,7 @@ export function useUserWorkflows() {
setWorkflows([]);
setPagination(null);
}
- }, [request]);
+ }, [request, apiBaseUrl]);
// Optimistically remove a workflow from the local state
const removeOptimistically = (workflowId: string) => {
@@ -185,13 +189,13 @@ export function useUserWorkflows() {
// Fetch a single workflow by ID
const fetchWorkflowById = useCallback(async (workflowId: string): Promise => {
try {
- const workflow = await fetchWorkflowByIdApi(request, workflowId);
+ const workflow = await fetchWorkflowByIdApi(request, workflowId, apiBaseUrl);
return workflow as UserWorkflow | null;
} catch (error: any) {
console.error('Error fetching workflow by ID:', error);
return null;
}
- }, [request]);
+ }, [request, apiBaseUrl]);
// Generate edit fields from attributes dynamically
const generateEditFieldsFromAttributes = useCallback((): Array<{
@@ -375,8 +379,9 @@ export function useUserWorkflows() {
};
}
-// Workflow operations hook
-export function useWorkflowOperations() {
+// Workflow operations hook - pass instanceId and featureCode when in feature context for feature-scoped API
+export function useWorkflowOperations(options?: { instanceId?: string; featureCode?: string }) {
+ const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode);
const [startingWorkflow, setStartingWorkflow] = useState(false);
const [stoppingWorkflows, setStoppingWorkflows] = useState>(new Set());
const [deletingWorkflows, setDeletingWorkflows] = useState>(new Set());
@@ -476,7 +481,7 @@ export function useWorkflowOperations() {
workflowId,
setDeletingWorkflows,
setDeleteError,
- () => deleteWorkflowApi(request, workflowId),
+ () => deleteWorkflowApi(request, workflowId, apiBaseUrl),
{
default: 'Failed to delete workflow',
notFound: 'Workflow not found or has already been deleted.',
@@ -512,7 +517,7 @@ export function useWorkflowOperations() {
try {
// 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
await new Promise(resolve => setTimeout(resolve, 300));
@@ -547,7 +552,7 @@ export function useWorkflowOperations() {
operationKey,
setDeletingMessages,
setDeleteMessageError,
- () => deleteMessageApi(request, workflowId, messageId),
+ () => deleteMessageApi(request, workflowId, messageId, apiBaseUrl),
{
default: 'Failed to delete message',
notFound: 'Message not found or has already been deleted.',
@@ -566,7 +571,7 @@ export function useWorkflowOperations() {
operationKey,
setDeletingFiles,
setDeleteFileError,
- () => deleteFileFromMessageApi(request, workflowId, messageId, fileId),
+ () => deleteFileFromMessageApi(request, workflowId, messageId, fileId, apiBaseUrl),
{
default: 'Failed to delete file',
notFound: 'File not found or has already been deleted.',
@@ -580,7 +585,7 @@ export function useWorkflowOperations() {
setEditingWorkflows(prev => new Set(prev).add(workflowId));
try {
- const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData);
+ const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData, apiBaseUrl);
return { success: true, workflowData: updatedWorkflow };
} catch (error: any) {
diff --git a/src/pages/views/automation/AutomationDefinitionsView.tsx b/src/pages/views/automation/AutomationDefinitionsView.tsx
index 3a4d492..a96bd9e 100644
--- a/src/pages/views/automation/AutomationDefinitionsView.tsx
+++ b/src/pages/views/automation/AutomationDefinitionsView.tsx
@@ -13,6 +13,7 @@ import { FaSync, FaRobot, FaRocket, FaPlus, FaFileAlt, FaStop, FaList, FaTimes,
import { useToast } from '../../../contexts/ToastContext';
import { useApiRequest } from '../../../hooks/useApi';
import { useFeatureStore } from '../../../stores/featureStore';
+import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import styles from '../../admin/Admin.module.css';
interface WorkflowLog {
@@ -24,11 +25,19 @@ interface WorkflowLog {
}
export const AutomationDefinitionsView: React.FC = () => {
+ const { instanceId: routeInstanceId, featureCode: routeFeatureCode } = useCurrentInstance();
const { getAllInstances } = useFeatureStore();
const instances = getAllInstances();
const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0];
- const mandateId = chatbotInstance?.mandateId;
- const featureInstanceId = chatbotInstance?.id;
+ const automationInstance = instances.find(i => i.featureCode === 'automation');
+ // 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 {
data: automations,
@@ -66,6 +75,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: boolean;
automationId: string | null;
automationLabel: string;
+ featureInstanceId: string | null;
workflowId: string | null;
status: 'starting' | 'running' | 'completed' | 'stopped' | 'error';
logs: WorkflowLog[];
@@ -73,6 +83,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: false,
automationId: null,
automationLabel: '',
+ featureInstanceId: null,
workflowId: null,
status: 'starting',
logs: [],
@@ -259,14 +270,16 @@ export const AutomationDefinitionsView: React.FC = () => {
setShowEditor(true);
};
- const pollWorkflowLogs = useCallback(async (workflowId: string) => {
+ const pollWorkflowLogs = useCallback(async (workflowId: string, instanceId: string) => {
try {
const contextHeaders: Record = {};
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({
- url: `/api/workflows/${workflowId}/logs`,
+ url: logsUrl,
method: 'get',
- params: lastLogIdRef.current ? { afterId: lastLogIdRef.current } : {},
+ params: lastLogIdRef.current ? { logId: lastLogIdRef.current } : {},
additionalConfig: { headers: contextHeaders },
});
const logs: WorkflowLog[] = response?.items || response || [];
@@ -279,7 +292,7 @@ export const AutomationDefinitionsView: React.FC = () => {
lastLogIdRef.current = logs[logs.length - 1].id;
}
const statusResponse = await request({
- url: `/api/workflows/${workflowId}`,
+ url: workflowUrl,
method: 'get',
additionalConfig: { headers: contextHeaders },
});
@@ -309,6 +322,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: true,
automationId: automation.id,
automationLabel: automation.label,
+ featureInstanceId: automation.featureInstanceId ?? automationWorkflowInstanceId ?? null,
workflowId: null,
status: 'starting',
logs: [{ id: 'init', timestamp: Date.now() / 1000, message: 'Automatisierung wird gestartet...' }],
@@ -316,14 +330,17 @@ export const AutomationDefinitionsView: React.FC = () => {
try {
const result = await handleAutomationExecute(automation.id);
const workflowId = result?.id;
- if (workflowId) {
+ const instanceId = automation.featureInstanceId ?? automationWorkflowInstanceId;
+ if (workflowId && instanceId) {
setExecutionModal(prev => ({
...prev,
workflowId,
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) {
setExecutionModal(prev => ({
@@ -337,11 +354,17 @@ export const AutomationDefinitionsView: React.FC = () => {
const handleStopWorkflow = async () => {
if (!executionModal.workflowId) return;
+ const instanceId = executionModal.featureInstanceId ?? automationWorkflowInstanceId;
+ if (!instanceId) {
+ showError('Keine Feature-Instanz für Stopp verfügbar');
+ return;
+ }
try {
const stopHeaders: Record = {};
if (mandateId) stopHeaders['X-Mandate-Id'] = mandateId;
+ const stopUrl = `/api/automations/${instanceId}/workflows/${executionModal.workflowId}/stop`;
await request({
- url: `/api/workflows/${executionModal.workflowId}/stop`,
+ url: stopUrl,
method: 'post',
additionalConfig: { headers: stopHeaders },
});
@@ -363,6 +386,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: false,
automationId: null,
automationLabel: '',
+ featureInstanceId: null,
workflowId: null,
status: 'starting',
logs: [],
diff --git a/src/pages/workflows/WorkflowsPage.tsx b/src/pages/workflows/WorkflowsPage.tsx
index 0fdcc84..d32fb08 100644
--- a/src/pages/workflows/WorkflowsPage.tsx
+++ b/src/pages/workflows/WorkflowsPage.tsx
@@ -6,11 +6,12 @@
*/
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 { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaList, FaPlay } from 'react-icons/fa';
import { useNavigate } from 'react-router-dom';
+import { useCurrentInstance } from '../../hooks/useCurrentInstance';
import styles from '../admin/Admin.module.css';
interface Workflow {
@@ -23,8 +24,12 @@ interface Workflow {
export const WorkflowsPage: React.FC = () => {
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 {
data: workflows,
attributes,
@@ -35,16 +40,16 @@ export const WorkflowsPage: React.FC = () => {
refetch,
fetchWorkflowById,
updateOptimistically,
- } = useUserWorkflows();
+ } = useUserWorkflows(workflowOptions);
- // Operations hook
+ // Operations hook - pass instance context when in feature route
const {
handleWorkflowDelete,
handleWorkflowDeleteMultiple,
handleWorkflowUpdate,
handleInlineUpdate,
deletingWorkflows,
- } = useWorkflowOperations();
+ } = useWorkflowOperations(workflowOptions);
const [editingWorkflow, setEditingWorkflow] = useState(null);
@@ -173,7 +178,7 @@ export const WorkflowsPage: React.FC = () => {