-
+ {!_skipViewHeader && (
+
+ )}
diff --git a/src/pages/Login.module.css b/src/pages/Login.module.css
index 16905c5..cb0ec8d 100644
--- a/src/pages/Login.module.css
+++ b/src/pages/Login.module.css
@@ -47,8 +47,8 @@
margin-top: 2rem;
padding: 2rem;
- border-radius: 25px;
- border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent);
+ border-radius: 10px;
+ border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1);
}
@@ -73,7 +73,7 @@
left: 16px;
top: 50%;
transform: translateY(-50%);
- color: var(--color-primary);
+ color: var(--color-gray, #718096);
font-size: 1rem;
pointer-events: none;
transition: all 0.3s ease;
@@ -101,10 +101,10 @@
height: 50px;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
- border-radius: 25px;
+ border-radius: 6px;
font-size: 1rem;
- transition: all 0.2s ease;
+ transition: all 0.15s ease;
background-color: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
@@ -147,7 +147,7 @@
width: 100%;
height: 50px;
padding: 12px 20px;
- border-radius: 25px;
+ border-radius: 6px;
font-size: 1rem;
font-weight: 500;
@@ -171,7 +171,7 @@
.loginButton {
background-color: var(--color-secondary);
- color: var(--color-text);
+ color: #fff;
}
.loginButton:hover {
@@ -179,21 +179,23 @@
}
.microsoftButton {
- background-color: var(--color-primary);
- color: var(--color-bg);
+ background-color: var(--color-gray-disabled, #CBD5E0);
+ color: var(--color-text);
}
.microsoftButton:hover {
- background-color: var(--color-primary-hover);
+ background-color: var(--color-gray, #718096);
+ color: #fff;
}
.googleButton {
- background-color: var(--color-primary);
- color: var(--color-bg);
+ background-color: var(--color-gray-disabled, #CBD5E0);
+ color: var(--color-text);
}
.googleButton:hover {
- background-color: var(--color-primary-hover);
+ background-color: var(--color-gray, #718096);
+ color: #fff;
}
.divider {
@@ -252,13 +254,13 @@
flex: 1;
height: 46px;
padding: 10px 16px;
- border-radius: 25px;
+ border-radius: 6px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
border: none;
background-color: var(--color-secondary);
- color: var(--color-text);
+ color: #fff;
transition: all 0.2s ease;
font-family: var(--font-family);
}
@@ -271,7 +273,7 @@
flex: 1;
height: 46px;
padding: 10px 16px;
- border-radius: 25px;
+ border-radius: 6px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
@@ -349,7 +351,7 @@ button:disabled {
width: 100%;
margin-top: 1.25rem;
padding: 1.25rem;
- border-radius: 20px;
+ border-radius: 10px;
}
.registerLink {
@@ -365,7 +367,7 @@ button:disabled {
.loginBox {
padding: 1rem;
- border-radius: 16px;
+ border-radius: 10px;
}
.input,
diff --git a/src/pages/PasswordResetRequest.module.css b/src/pages/PasswordResetRequest.module.css
index 7b12a8b..98397a2 100644
--- a/src/pages/PasswordResetRequest.module.css
+++ b/src/pages/PasswordResetRequest.module.css
@@ -47,8 +47,8 @@
margin-top: 2rem;
padding: 2rem;
- border-radius: 25px;
- border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent);
+ border-radius: 8px;
+ border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1);
}
@@ -105,7 +105,7 @@
height: 50px;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
- border-radius: 25px;
+ border-radius: 8px;
font-size: 1rem;
transition: all 0.2s ease;
@@ -145,7 +145,7 @@
width: 100%;
height: 50px;
padding: 12px 20px;
- border-radius: 25px;
+ border-radius: 8px;
font-size: 1rem;
font-weight: 500;
@@ -200,7 +200,7 @@ button:disabled {
color: var(--color-secondary);
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
- border-radius: 25px;
+ border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
@@ -212,7 +212,7 @@ button:disabled {
color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 10%, transparent);
border: 1px solid var(--color-success);
- border-radius: 25px;
+ border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
diff --git a/src/pages/Register.module.css b/src/pages/Register.module.css
index a0350a7..c967413 100644
--- a/src/pages/Register.module.css
+++ b/src/pages/Register.module.css
@@ -47,8 +47,8 @@
margin-top: 2rem;
padding: 2rem;
- border-radius: 25px;
- border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent);
+ border-radius: 8px;
+ border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1);
}
@@ -101,7 +101,7 @@
height: 50px;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
- border-radius: 25px;
+ border-radius: 8px;
font-size: 1rem;
transition: all 0.2s ease;
@@ -152,7 +152,7 @@
width: 100%;
height: 50px;
padding: 12px 20px;
- border-radius: 25px;
+ border-radius: 8px;
font-size: 1rem;
font-weight: 500;
@@ -214,7 +214,7 @@ button:disabled {
color: var(--color-secondary);
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
- border-radius: 25px;
+ border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
@@ -226,7 +226,7 @@ button:disabled {
color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 10%, transparent);
border: 1px solid var(--color-success);
- border-radius: 25px;
+ border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
diff --git a/src/pages/Reset.module.css b/src/pages/Reset.module.css
index 18bda8b..49e3dfb 100644
--- a/src/pages/Reset.module.css
+++ b/src/pages/Reset.module.css
@@ -47,8 +47,8 @@
margin-top: 2rem;
padding: 2rem;
- border-radius: 25px;
- border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent);
+ border-radius: 8px;
+ border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1);
}
@@ -106,7 +106,7 @@
height: 50px;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
- border-radius: 25px;
+ border-radius: 8px;
font-size: 1rem;
transition: all 0.2s ease;
@@ -153,7 +153,7 @@
width: 100%;
height: 50px;
padding: 12px 20px;
- border-radius: 25px;
+ border-radius: 8px;
font-size: 1rem;
font-weight: 500;
@@ -209,7 +209,7 @@ button:disabled {
color: var(--color-secondary);
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
- border-radius: 25px;
+ border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
@@ -221,7 +221,7 @@ button:disabled {
color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 10%, transparent);
border: 1px solid var(--color-success);
- border-radius: 25px;
+ border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorDashboardPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorDashboardPage.tsx
index d985c6c..5b95772 100644
--- a/src/pages/views/graphicalEditor/GraphicalEditorDashboardPage.tsx
+++ b/src/pages/views/graphicalEditor/GraphicalEditorDashboardPage.tsx
@@ -6,7 +6,7 @@
*/
import React, { useState, useCallback, useEffect } from 'react';
-import { FaSync, FaPlay, FaCog, FaClipboardList, FaChartBar } from 'react-icons/fa';
+import { FaSync, FaPlay, FaCog, FaClipboardList, FaChartBar, FaDownload } from 'react-icons/fa';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi';
@@ -110,6 +110,36 @@ export const GraphicalEditorDashboardPage: React.FC = () => {
load();
}, [load]);
+ const _downloadRunTracing = useCallback(async (run: CompletedRun) => {
+ if (!instanceId || !run.id) return;
+ try {
+ const data = await request({
+ url: `/api/workflows/${instanceId}/runs/${run.id}/steps`,
+ method: 'get',
+ });
+ const steps = data?.steps || [];
+ const report = {
+ runId: run.id,
+ workflowId: run.workflowId,
+ workflowLabel: run.workflowLabel,
+ status: run.status,
+ startedAt: _formatTs(run.sysCreatedAt),
+ endedAt: _formatTs(run.sysModifiedAt),
+ steps,
+ };
+ const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `run-tracing-${run.id.slice(0, 8)}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ } catch (e) {
+ console.error('[dashboard] download tracing failed', e);
+ showError('Download fehlgeschlagen');
+ }
+ }, [instanceId, request, showError]);
+
const runColumns: ColumnConfig[] = [
{
key: 'workflowLabel',
@@ -144,6 +174,26 @@ export const GraphicalEditorDashboardPage: React.FC = () => {
width: 150,
formatter: (v: number) => _formatTs(v),
},
+ {
+ key: 'id',
+ label: '',
+ type: 'string',
+ width: 50,
+ sortable: false,
+ formatter: (_v: string, row: CompletedRun) => (
+
+ ),
+ },
];
if (!instanceId) {
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorPage.tsx
index b1371a3..e5ca004 100644
--- a/src/pages/views/graphicalEditor/GraphicalEditorPage.tsx
+++ b/src/pages/views/graphicalEditor/GraphicalEditorPage.tsx
@@ -1,25 +1,16 @@
/**
* GraphicalEditorPage
*
- * Layout: [UDB sidebar (collapsible)] [FlowEditor (flex)] [Chat/Tracing (inside FlowEditor)]
- * UDB provides access to Files & Sources while configuring nodes.
- * AI Chat and Tracing panels are managed by the FlowEditor's CanvasHeader.
- *
- * File/Source attachment UX mirrors the Workspace:
- * - Files: click in UDB FilesTab → added as pendingFile chip in chat input
- * - Data Sources: 🔗 picker button in chat input (loaded from UDB API)
+ * Thin wrapper: passes instance context to FlowEditor which now owns the full layout
+ * including the Workspace panel (Chats/Dateien/Quellen) on the left.
*/
-import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
+import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
-import { FaDatabase, FaChevronLeft } from 'react-icons/fa';
import { useInstanceId, useMandateId } from '../../../hooks/useCurrentInstance';
import { useLanguage } from '../../../providers/language/LanguageContext';
import { Automation2FlowEditor as FlowEditor } from '../../../components/FlowEditor';
import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from '../../../components/FlowEditor';
-import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
-import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
import api from '../../../api';
-import styles from '../../FeatureView.module.css';
interface GraphicalEditorPageProps {
persistentInstanceId?: string;
@@ -35,22 +26,24 @@ export const GraphicalEditorPage: React.FC
= ({
const instanceId = persistentInstanceId || urlInstanceId;
const mandateId = persistentMandateId || urlMandateId;
const [searchParams] = useSearchParams();
- const initialWorkflowIdRef = useRef(searchParams.get('workflowId'));
+ const workflowIdFromUrl = searchParams.get('workflowId');
+ const [activeWorkflowId, setActiveWorkflowId] = useState(workflowIdFromUrl);
+ const prevWorkflowIdRef = useRef(workflowIdFromUrl);
+
+ useEffect(() => {
+ if (workflowIdFromUrl && workflowIdFromUrl !== prevWorkflowIdRef.current) {
+ prevWorkflowIdRef.current = workflowIdFromUrl;
+ setActiveWorkflowId(workflowIdFromUrl);
+ }
+ }, [workflowIdFromUrl]);
+
const { currentLanguage } = useLanguage();
const language = (currentLanguage?.slice(0, 2) || 'de') as string;
- const [udbTab, setUdbTab] = useState('files');
- const [udbOpen, setUdbOpen] = useState(true);
const [pendingFiles, setPendingFiles] = useState([]);
const [dataSources, setDataSources] = useState([]);
const [featureDataSources, setFeatureDataSources] = useState([]);
- const udbContext: UdbContext = useMemo(() => ({
- instanceId: instanceId || '',
- mandateId: mandateId || '',
- featureInstanceId: instanceId || '',
- }), [instanceId, mandateId]);
-
useEffect(() => {
if (!instanceId) return;
api.get(`/api/workspace/${instanceId}/datasources`)
@@ -108,7 +101,7 @@ export const GraphicalEditorPage: React.FC = ({
if (!instanceId) {
return (
-
+
Graphical Editor
Keine Feature-Instanz gefunden.
@@ -116,75 +109,19 @@ export const GraphicalEditorPage: React.FC
= ({
}
return (
-
- {/* UDB Sidebar */}
- {udbOpen ? (
-
-
-
- Daten
-
-
-
-
-
- ) : (
-
- )}
-
- {/* FlowEditor */}
-
-
-
+
+
);
};
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
index 64431b8..178695a 100644
--- a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
+++ b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
@@ -8,7 +8,8 @@
import React, { useState, useCallback, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import { FaCopy, FaSync, FaShareAlt } from 'react-icons/fa';
+import { FaCopy, FaSync, FaShareAlt, FaPen } from 'react-icons/fa';
+import { usePrompt } from '../../../hooks/usePrompt';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi';
@@ -17,6 +18,7 @@ import {
copyTemplate,
shareTemplate,
deleteWorkflow,
+ updateWorkflow,
type AutoWorkflowTemplate,
type AutoTemplateScope,
} from '../../../api/workflowApi';
@@ -50,6 +52,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
const { request } = useApiRequest();
const navigate = useNavigate();
const { showSuccess, showError } = useToast();
+ const { prompt: promptInput, PromptDialog } = usePrompt();
const [templates, setTemplates] = useState
([]);
const [loading, setLoading] = useState(true);
@@ -117,18 +120,15 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
);
const handleShare = useCallback(
- async (row: AutoWorkflowTemplate) => {
+ async (row: AutoWorkflowTemplate, targetScope: AutoTemplateScope) => {
if (!instanceId) return;
- const currentScope = row.templateScope || 'user';
- const nextScope: AutoTemplateScope =
- currentScope === 'user' ? 'instance' : currentScope === 'instance' ? 'mandate' : 'mandate';
setSharingId(row.id);
try {
- await shareTemplate(request, instanceId, row.id, nextScope);
- showSuccess(`Vorlage freigegeben (Scope: ${SCOPE_LABELS[nextScope]})`);
+ await shareTemplate(request, instanceId, row.id, targetScope);
+ showSuccess(`Scope geändert: ${SCOPE_LABELS[targetScope]}`);
await load();
} catch (e: any) {
- showError(`Fehler: ${e?.message || 'Freigabe fehlgeschlagen'}`);
+ showError(`Fehler: ${e?.message || 'Scope-Änderung fehlgeschlagen'}`);
} finally {
setSharingId(null);
}
@@ -136,6 +136,28 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
[instanceId, request, showSuccess, showError, load]
);
+ const [scopeMenuId, setScopeMenuId] = useState(null);
+
+ const handleRename = useCallback(
+ async (row: AutoWorkflowTemplate) => {
+ if (!instanceId) return;
+ const newLabel = await promptInput('Neuer Name:', {
+ title: 'Vorlage umbenennen',
+ defaultValue: row.label,
+ placeholder: 'Vorlagen-Name',
+ });
+ if (!newLabel || newLabel.trim() === row.label) return;
+ try {
+ await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() });
+ showSuccess('Vorlage umbenannt');
+ await load();
+ } catch (e: any) {
+ showError(`Fehler: ${e?.message || 'Umbenennen fehlgeschlagen'}`);
+ }
+ },
+ [instanceId, request, promptInput, showSuccess, showError, load]
+ );
+
const handleEdit = useCallback(
(row: AutoWorkflowTemplate) => {
if (!mandateId || !instanceId) return;
@@ -243,6 +265,13 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
},
]}
customActions={[
+ {
+ id: 'rename',
+ icon: ,
+ title: 'Umbenennen',
+ onClick: (row) => handleRename(row),
+ visible: (row) => (row.templateScope || 'user') !== 'system',
+ },
{
id: 'copy',
icon: ,
@@ -251,10 +280,10 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
loading: (row) => copyingId === row.id,
},
{
- id: 'share',
+ id: 'scope',
icon: ,
- title: 'Scope erweitern (freigeben)',
- onClick: (row) => handleShare(row),
+ title: 'Scope ändern',
+ onClick: (row) => setScopeMenuId(scopeMenuId === row.id ? null : row.id),
loading: (row) => sharingId === row.id,
visible: (row) => (row.templateScope || 'user') !== 'system',
},
@@ -264,6 +293,59 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
emptyMessage="Keine Vorlagen gefunden. Erstelle eine Vorlage aus einem bestehenden Workflow."
/>
+
+ {/* Scope change dropdown overlay */}
+ {scopeMenuId && (() => {
+ const tpl = templates.find(t => t.id === scopeMenuId);
+ if (!tpl) return null;
+ const currentScope = (tpl.templateScope || 'user') as AutoTemplateScope;
+ const scopes: AutoTemplateScope[] = ['user', 'instance', 'mandate'];
+ return (
+ setScopeMenuId(null)}
+ >
+
e.stopPropagation()}
+ >
+
Scope ändern
+
+ Aktuell: {SCOPE_LABELS[currentScope]}
+
+
+ {scopes.map(s => (
+
+ ))}
+
+
+
+
+ );
+ })()}
+
+
);
};
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx
index c34f3cc..58d6ef1 100644
--- a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx
+++ b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx
@@ -8,7 +8,8 @@
import React, { useState, useCallback, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import { FaPlay, FaSync, FaCheck, FaBan } from 'react-icons/fa';
+import { FaPlay, FaSync, FaCheck, FaBan, FaPen } from 'react-icons/fa';
+import { usePrompt } from '../../../hooks/usePrompt';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi';
@@ -42,6 +43,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
const { request } = useApiRequest();
const navigate = useNavigate();
const { showSuccess, showError } = useToast();
+ const { prompt: promptInput, PromptDialog } = usePrompt();
const [workflows, setWorkflows] = useState([]);
const [loading, setLoading] = useState(true);
@@ -123,6 +125,26 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
[instanceId, request, showSuccess, showError, load]
);
+ const handleRename = useCallback(
+ async (row: Automation2Workflow) => {
+ if (!instanceId) return;
+ const newLabel = await promptInput('Neuer Name:', {
+ title: 'Workflow umbenennen',
+ defaultValue: row.label,
+ placeholder: 'Workflow-Name',
+ });
+ if (!newLabel || newLabel.trim() === row.label) return;
+ try {
+ await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() });
+ showSuccess('Workflow umbenannt');
+ await load();
+ } catch (e: any) {
+ showError(`Fehler: ${e?.message || 'Umbenennen fehlgeschlagen'}`);
+ }
+ },
+ [instanceId, request, promptInput, showSuccess, showError, load]
+ );
+
const handleExecute = useCallback(
async (row: Automation2Workflow) => {
if (!instanceId) return;
@@ -282,6 +304,12 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
},
]}
customActions={[
+ {
+ id: 'rename',
+ icon: ,
+ title: 'Umbenennen',
+ onClick: (row) => handleRename(row),
+ },
{
id: 'activate',
icon: ,
@@ -312,6 +340,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
emptyMessage="Keine Workflows gefunden. Erstelle einen im Editor."
/>
+