automation template management and fix admin center
This commit is contained in:
parent
1863ca9fd0
commit
8d86c166d0
12 changed files with 1128 additions and 41 deletions
|
|
@ -44,7 +44,7 @@ import { FeatureViewPage } from './pages/FeatureView';
|
|||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage } from './pages/admin';
|
||||
|
||||
// Workflow Pages (global)
|
||||
import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows';
|
||||
import { PlaygroundPage, WorkflowsPage, AutomationsPage, AutomationTemplatesPage } from './pages/workflows';
|
||||
|
||||
// Basedata Pages (global)
|
||||
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
||||
|
|
@ -114,6 +114,7 @@ function App() {
|
|||
<Route path="playground" element={<PlaygroundPage />} />
|
||||
<Route path="list" element={<WorkflowsPage />} />
|
||||
<Route path="automations" element={<AutomationsPage />} />
|
||||
<Route path="automation-templates" element={<AutomationTemplatesPage />} />
|
||||
</Route>
|
||||
|
||||
{/* ============================================== */}
|
||||
|
|
|
|||
|
|
@ -32,17 +32,49 @@ export interface AutomationLog {
|
|||
messages?: string[];
|
||||
}
|
||||
|
||||
// Multilingual text type (matches backend TextMultilingual)
|
||||
export interface TextMultilingual {
|
||||
en: string;
|
||||
ge?: string;
|
||||
fr?: string;
|
||||
it?: string;
|
||||
}
|
||||
|
||||
// AutomationTemplate from DB
|
||||
export interface AutomationTemplate {
|
||||
template: {
|
||||
overview?: string;
|
||||
tasks?: Array<{
|
||||
description?: string;
|
||||
objective?: string;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
label: TextMultilingual;
|
||||
overview?: TextMultilingual;
|
||||
template: string; // JSON string with {{KEY:...}} placeholders
|
||||
_createdAt?: number;
|
||||
_createdBy?: string;
|
||||
_createdByUserName?: string;
|
||||
}
|
||||
|
||||
// Workflow action definition from backend
|
||||
export interface WorkflowAction {
|
||||
method: string;
|
||||
action: string;
|
||||
actionId: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
parameters: WorkflowActionParameter[];
|
||||
exampleJson: {
|
||||
execMethod: string;
|
||||
execAction: string;
|
||||
execParameters: Record<string, any>;
|
||||
execResultLabel: string;
|
||||
};
|
||||
parameters?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface WorkflowActionParameter {
|
||||
name: string;
|
||||
type: string;
|
||||
frontendType: string;
|
||||
required: boolean;
|
||||
default?: any;
|
||||
description: string;
|
||||
frontendOptions?: string | string[];
|
||||
}
|
||||
|
||||
export interface CreateAutomationRequest {
|
||||
|
|
@ -188,34 +220,6 @@ export async function executeAutomationApi(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch automation templates
|
||||
* Endpoint: GET /api/automations/templates
|
||||
*/
|
||||
export async function fetchAutomationTemplates(
|
||||
request: ApiRequestFunction
|
||||
): Promise<AutomationTemplate[]> {
|
||||
const data = await request({
|
||||
url: '/api/automations/templates',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data && typeof data === 'object') {
|
||||
if (Array.isArray(data.sets)) {
|
||||
return data.sets;
|
||||
}
|
||||
if (Array.isArray(data.templates)) {
|
||||
return data.templates;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch automation attributes for dynamic form generation
|
||||
* Endpoint: GET /api/attributes/AutomationDefinition
|
||||
|
|
@ -238,3 +242,133 @@ export async function fetchAutomationAttributes(
|
|||
|
||||
return [];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUTOMATION TEMPLATES API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch all automation templates (RBAC-filtered: own templates)
|
||||
* Endpoint: GET /api/automation-templates
|
||||
*/
|
||||
export async function fetchAutomationTemplates(
|
||||
request: ApiRequestFunction
|
||||
): Promise<AutomationTemplate[]> {
|
||||
const data = await request({
|
||||
url: '/api/automation-templates',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
if (data?.items && Array.isArray(data.items)) {
|
||||
return data.items;
|
||||
}
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single automation template by ID
|
||||
* Endpoint: GET /api/automation-templates/{templateId}
|
||||
*/
|
||||
export async function fetchAutomationTemplateById(
|
||||
request: ApiRequestFunction,
|
||||
templateId: string
|
||||
): Promise<AutomationTemplate | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/automation-templates/${templateId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching template:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new automation template
|
||||
* Endpoint: POST /api/automation-templates
|
||||
*/
|
||||
export async function createAutomationTemplateApi(
|
||||
request: ApiRequestFunction,
|
||||
templateData: Omit<AutomationTemplate, 'id' | '_createdAt' | '_createdBy'>
|
||||
): Promise<AutomationTemplate> {
|
||||
return await request({
|
||||
url: '/api/automation-templates',
|
||||
method: 'post',
|
||||
data: templateData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update automation template
|
||||
* Endpoint: PUT /api/automation-templates/{templateId}
|
||||
*/
|
||||
export async function updateAutomationTemplateApi(
|
||||
request: ApiRequestFunction,
|
||||
templateId: string,
|
||||
templateData: Partial<AutomationTemplate>
|
||||
): Promise<AutomationTemplate> {
|
||||
return await request({
|
||||
url: `/api/automation-templates/${templateId}`,
|
||||
method: 'put',
|
||||
data: templateData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete automation template
|
||||
* Endpoint: DELETE /api/automation-templates/{templateId}
|
||||
*/
|
||||
export async function deleteAutomationTemplateApi(
|
||||
request: ApiRequestFunction,
|
||||
templateId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/automation-templates/${templateId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch automation template attributes for dynamic form generation
|
||||
* Endpoint: GET /api/automation-templates/attributes
|
||||
*/
|
||||
export async function fetchAutomationTemplateAttributes(
|
||||
request: ApiRequestFunction
|
||||
): Promise<any[]> {
|
||||
const data = await request({
|
||||
url: '/api/automation-templates/attributes',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
// Backend returns: { attributes: { model: "...", attributes: [...] } }
|
||||
if (data?.attributes?.attributes && Array.isArray(data.attributes.attributes)) {
|
||||
return data.attributes.attributes;
|
||||
}
|
||||
|
||||
// Fallback: direct attributes array
|
||||
if (data?.attributes && Array.isArray(data.attributes)) {
|
||||
return data.attributes;
|
||||
}
|
||||
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WORKFLOW ACTIONS API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch available workflow actions (RBAC-filtered)
|
||||
* Endpoint: GET /api/automations/actions
|
||||
*/
|
||||
export async function fetchWorkflowActions(
|
||||
request: ApiRequestFunction
|
||||
): Promise<WorkflowAction[]> {
|
||||
const data = await request({
|
||||
url: '/api/automations/actions',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return data?.actions || [];
|
||||
}
|
||||
|
|
|
|||
289
src/components/ActionsPanel/ActionsPanel.module.css
Normal file
289
src/components/ActionsPanel/ActionsPanel.module.css
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/* ActionsPanel Styles */
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
background: var(--bg-primary, #ffffff);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
color: var(--text-secondary, #666);
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary, #333);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.searchInput::placeholder {
|
||||
color: var(--text-tertiary, #999);
|
||||
}
|
||||
|
||||
.actionsList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color, #dc3545);
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary-color, #007bff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.retryButton:hover {
|
||||
background: var(--primary-hover, #0056b3);
|
||||
}
|
||||
|
||||
/* Method Groups */
|
||||
.methodGroup {
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--bg-primary, #ffffff);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.methodHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #333);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.methodHeader:hover {
|
||||
background: var(--bg-hover, #f0f0f0);
|
||||
}
|
||||
|
||||
.methodHeader svg {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.methodName {
|
||||
flex: 1;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.methodCount {
|
||||
background: var(--primary-color, #007bff);
|
||||
color: white;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Method Actions */
|
||||
.methodActions {
|
||||
border-top: 1px solid var(--border-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.actionItem {
|
||||
border-bottom: 1px solid var(--border-light, #f0f0f0);
|
||||
}
|
||||
|
||||
.actionItem:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.actionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.actionHeader:hover {
|
||||
background: var(--bg-hover, #f5f5f5);
|
||||
}
|
||||
|
||||
.actionInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.actionName {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.actionDesc {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #666);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary, #666);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.copyButton:hover {
|
||||
background: var(--primary-color, #007bff);
|
||||
border-color: var(--primary-color, #007bff);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Action Details */
|
||||
.actionDetails {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-secondary, #f8f9fa);
|
||||
border-top: 1px solid var(--border-light, #f0f0f0);
|
||||
}
|
||||
|
||||
.actionDetails h5 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #666);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Parameters */
|
||||
.parameters {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.parameters ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.param {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.paramName {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--error-color, #dc3545);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.paramType {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
background: var(--bg-code, #e9ecef);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 3px;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.paramDesc {
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-tertiary, #888);
|
||||
}
|
||||
|
||||
/* Example JSON */
|
||||
.exampleJson {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.exampleJson pre {
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-code, #1e1e1e);
|
||||
color: var(--text-code, #d4d4d4);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.insertButton {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary-color, #007bff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.insertButton:hover {
|
||||
background: var(--primary-hover, #0056b3);
|
||||
}
|
||||
216
src/components/ActionsPanel/ActionsPanel.tsx
Normal file
216
src/components/ActionsPanel/ActionsPanel.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* ActionsPanel
|
||||
*
|
||||
* Displays available workflow actions for copy/paste into templates.
|
||||
* Groups actions by method and shows parameters + example JSON.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useWorkflowActions, type WorkflowAction } from '../../hooks/useAutomations';
|
||||
import { FaSearch, FaCopy, FaChevronDown, FaChevronRight, FaCheck } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import styles from './ActionsPanel.module.css';
|
||||
|
||||
interface ActionsPanelProps {
|
||||
/** Callback when action JSON is inserted (optional) */
|
||||
onInsert?: (actionJson: string) => void;
|
||||
/** Callback when action JSON is copied (optional) */
|
||||
onCopy?: (actionJson: string) => void;
|
||||
}
|
||||
|
||||
export const ActionsPanel: React.FC<ActionsPanelProps> = ({ onInsert, onCopy }) => {
|
||||
const { actions, loading, error, fetchActions } = useWorkflowActions();
|
||||
const { showSuccess } = useToast();
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
const [expandedMethods, setExpandedMethods] = useState<Set<string>>(new Set());
|
||||
const [expandedAction, setExpandedAction] = useState<string | null>(null);
|
||||
const [copiedAction, setCopiedAction] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchActions();
|
||||
}, [fetchActions]);
|
||||
|
||||
// Filter actions by search term
|
||||
const filteredActions = useMemo(() => {
|
||||
if (!filter) return actions;
|
||||
const lower = filter.toLowerCase();
|
||||
return actions.filter(a =>
|
||||
a.method.toLowerCase().includes(lower) ||
|
||||
a.action.toLowerCase().includes(lower) ||
|
||||
a.description.toLowerCase().includes(lower) ||
|
||||
a.actionId.toLowerCase().includes(lower)
|
||||
);
|
||||
}, [actions, filter]);
|
||||
|
||||
// Group actions by method
|
||||
const groupedActions = useMemo(() => {
|
||||
const groups: Record<string, WorkflowAction[]> = {};
|
||||
filteredActions.forEach(action => {
|
||||
if (!groups[action.method]) {
|
||||
groups[action.method] = [];
|
||||
}
|
||||
groups[action.method].push(action);
|
||||
});
|
||||
return groups;
|
||||
}, [filteredActions]);
|
||||
|
||||
// Toggle method expansion
|
||||
const toggleMethod = (method: string) => {
|
||||
setExpandedMethods(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(method)) {
|
||||
newSet.delete(method);
|
||||
} else {
|
||||
newSet.add(method);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Toggle action details
|
||||
const toggleAction = (actionId: string) => {
|
||||
setExpandedAction(prev => prev === actionId ? null : actionId);
|
||||
};
|
||||
|
||||
// Copy action JSON to clipboard
|
||||
const handleCopy = async (action: WorkflowAction) => {
|
||||
const json = JSON.stringify(action.exampleJson, null, 2);
|
||||
try {
|
||||
await navigator.clipboard.writeText(json);
|
||||
setCopiedAction(action.actionId);
|
||||
setTimeout(() => setCopiedAction(null), 2000);
|
||||
showSuccess('JSON kopiert');
|
||||
onCopy?.(json);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Insert action JSON
|
||||
const handleInsert = (action: WorkflowAction) => {
|
||||
const json = JSON.stringify(action.exampleJson, null, 2);
|
||||
onInsert?.(json);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.loading}>Lade Actions...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.error}>Fehler: {error}</div>
|
||||
<button className={styles.retryButton} onClick={() => fetchActions()}>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>Verfügbare Actions</h3>
|
||||
<div className={styles.searchBox}>
|
||||
<FaSearch className={styles.searchIcon} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionsList}>
|
||||
{Object.keys(groupedActions).length === 0 ? (
|
||||
<div className={styles.empty}>Keine Actions gefunden</div>
|
||||
) : (
|
||||
Object.entries(groupedActions).map(([method, methodActions]) => (
|
||||
<div key={method} className={styles.methodGroup}>
|
||||
<button
|
||||
className={styles.methodHeader}
|
||||
onClick={() => toggleMethod(method)}
|
||||
>
|
||||
{expandedMethods.has(method) ? <FaChevronDown /> : <FaChevronRight />}
|
||||
<span className={styles.methodName}>{method}</span>
|
||||
<span className={styles.methodCount}>{methodActions.length}</span>
|
||||
</button>
|
||||
|
||||
{expandedMethods.has(method) && (
|
||||
<div className={styles.methodActions}>
|
||||
{methodActions.map(action => (
|
||||
<div key={action.actionId} className={styles.actionItem}>
|
||||
<div
|
||||
className={styles.actionHeader}
|
||||
onClick={() => toggleAction(action.actionId)}
|
||||
>
|
||||
<div className={styles.actionInfo}>
|
||||
<span className={styles.actionName}>{action.action}</span>
|
||||
<span className={styles.actionDesc}>{action.description}</span>
|
||||
</div>
|
||||
<button
|
||||
className={styles.copyButton}
|
||||
onClick={(e) => { e.stopPropagation(); handleCopy(action); }}
|
||||
title="JSON kopieren"
|
||||
>
|
||||
{copiedAction === action.actionId ? <FaCheck /> : <FaCopy />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expandedAction === action.actionId && (
|
||||
<div className={styles.actionDetails}>
|
||||
{action.parameters.length > 0 && (
|
||||
<div className={styles.parameters}>
|
||||
<h5>Parameter:</h5>
|
||||
<ul>
|
||||
{action.parameters.map(param => (
|
||||
<li key={param.name} className={styles.param}>
|
||||
<span className={styles.paramName}>
|
||||
{param.name}
|
||||
{param.required && <span className={styles.required}>*</span>}
|
||||
</span>
|
||||
<span className={styles.paramType}>{param.type}</span>
|
||||
{param.description && (
|
||||
<span className={styles.paramDesc}>{param.description}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.exampleJson}>
|
||||
<h5>Beispiel JSON:</h5>
|
||||
<pre>{JSON.stringify(action.exampleJson, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
{onInsert && (
|
||||
<button
|
||||
className={styles.insertButton}
|
||||
onClick={() => handleInsert(action)}
|
||||
>
|
||||
In Template einfügen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionsPanel;
|
||||
2
src/components/ActionsPanel/index.ts
Normal file
2
src/components/ActionsPanel/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { ActionsPanel } from './ActionsPanel';
|
||||
export { default } from './ActionsPanel';
|
||||
|
|
@ -39,6 +39,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
'page.system.playground': <FaPlay />,
|
||||
'page.system.chats': <FaListAlt />,
|
||||
'page.system.automations': <FaCogs />,
|
||||
'page.system.automation-templates': <FaFileAlt />,
|
||||
'page.system.prompts': <FaLightbulb />,
|
||||
'page.system.files': <FaRegFileAlt />,
|
||||
'page.system.connections': <FaLink />,
|
||||
|
|
|
|||
|
|
@ -10,14 +10,29 @@ import {
|
|||
deleteAutomationApi,
|
||||
executeAutomationApi,
|
||||
fetchAutomationTemplates as fetchTemplatesApi,
|
||||
fetchAutomationTemplateById,
|
||||
createAutomationTemplateApi,
|
||||
updateAutomationTemplateApi,
|
||||
deleteAutomationTemplateApi,
|
||||
fetchAutomationTemplateAttributes,
|
||||
fetchWorkflowActions as fetchWorkflowActionsApi,
|
||||
type Automation,
|
||||
type AutomationTemplate,
|
||||
type TextMultilingual,
|
||||
type WorkflowAction,
|
||||
type CreateAutomationRequest,
|
||||
type UpdateAutomationRequest
|
||||
} from '../api/automationApi';
|
||||
|
||||
// Re-export types
|
||||
export type { Automation, AutomationTemplate, CreateAutomationRequest, UpdateAutomationRequest };
|
||||
export type {
|
||||
Automation,
|
||||
AutomationTemplate,
|
||||
TextMultilingual,
|
||||
WorkflowAction,
|
||||
CreateAutomationRequest,
|
||||
UpdateAutomationRequest
|
||||
};
|
||||
|
||||
// Attribute definition interface
|
||||
export interface AttributeDefinition {
|
||||
|
|
@ -446,3 +461,143 @@ export function useAutomationOperations() {
|
|||
updateError
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUTOMATION TEMPLATES (DB) HOOK
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook for managing AutomationTemplates from database
|
||||
*/
|
||||
export function useAutomationTemplates() {
|
||||
const [templates, setTemplates] = useState<AutomationTemplate[]>([]);
|
||||
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { request } = useApiRequest();
|
||||
const { checkPermission } = usePermissions();
|
||||
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
||||
|
||||
const fetchTemplates = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await fetchTemplatesApi(request);
|
||||
setTemplates(data);
|
||||
} catch (e: any) {
|
||||
console.error('Error fetching templates:', e);
|
||||
setError(e.message || 'Failed to fetch templates');
|
||||
setTemplates([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
const fetchAttributes = useCallback(async () => {
|
||||
try {
|
||||
const attrs = await fetchAutomationTemplateAttributes(request);
|
||||
setAttributes(attrs);
|
||||
return attrs;
|
||||
} catch (e: any) {
|
||||
console.error('Error fetching template attributes:', e);
|
||||
setAttributes([]);
|
||||
return [];
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
const fetchPermissions = useCallback(async () => {
|
||||
try {
|
||||
const perms = await checkPermission('DATA', 'AutomationTemplate');
|
||||
setPermissions(perms);
|
||||
return perms;
|
||||
} catch (e: any) {
|
||||
console.error('Error fetching template permissions:', e);
|
||||
const defaultPerms: UserPermissions = {
|
||||
view: false,
|
||||
read: 'n',
|
||||
create: 'n',
|
||||
update: 'n',
|
||||
delete: 'n',
|
||||
};
|
||||
setPermissions(defaultPerms);
|
||||
return defaultPerms;
|
||||
}
|
||||
}, [checkPermission]);
|
||||
|
||||
const getTemplate = useCallback(async (templateId: string) => {
|
||||
return await fetchAutomationTemplateById(request, templateId);
|
||||
}, [request]);
|
||||
|
||||
const createTemplate = useCallback(async (data: Omit<AutomationTemplate, 'id' | '_createdAt' | '_createdBy'>) => {
|
||||
return await createAutomationTemplateApi(request, data);
|
||||
}, [request]);
|
||||
|
||||
const updateTemplate = useCallback(async (templateId: string, data: Partial<AutomationTemplate>) => {
|
||||
return await updateAutomationTemplateApi(request, templateId, data);
|
||||
}, [request]);
|
||||
|
||||
const deleteTemplate = useCallback(async (templateId: string) => {
|
||||
await deleteAutomationTemplateApi(request, templateId);
|
||||
}, [request]);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
await Promise.all([
|
||||
fetchTemplates(),
|
||||
fetchAttributes(),
|
||||
fetchPermissions()
|
||||
]);
|
||||
}, [fetchTemplates, fetchAttributes, fetchPermissions]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
data: templates, // Alias for FormGenerator compatibility
|
||||
attributes,
|
||||
loading,
|
||||
error,
|
||||
permissions,
|
||||
refetch,
|
||||
fetchTemplates,
|
||||
fetchAttributes,
|
||||
fetchPermissions,
|
||||
getTemplate,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WORKFLOW ACTIONS HOOK
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook for fetching available workflow actions (for Actions panel)
|
||||
*/
|
||||
export function useWorkflowActions() {
|
||||
const [actions, setActions] = useState<WorkflowAction[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { request } = useApiRequest();
|
||||
|
||||
const fetchActions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await fetchWorkflowActionsApi(request);
|
||||
setActions(data);
|
||||
} catch (e: any) {
|
||||
console.error('Error fetching workflow actions:', e);
|
||||
setError(e.message || 'Failed to fetch actions');
|
||||
setActions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
return {
|
||||
actions,
|
||||
loading,
|
||||
error,
|
||||
fetchActions
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,6 +398,9 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<Link to="/admin/mandates" className={hubStyles.mandatesLink}>
|
||||
<FaBuilding /> Mandanten verwalten
|
||||
</Link>
|
||||
<Link to="/admin/user-mandates" className={hubStyles.mandatesLink}>
|
||||
<FaUsers /> Mandant-Benutzer
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{viewMode === 'hierarchy' ? (
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAdminMandates, type Mandate } from '../../hooks/useMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaBuilding } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync, FaBuilding, FaUsers } from 'react-icons/fa';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
export const AdminMandatesPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
mandates,
|
||||
attributes,
|
||||
|
|
@ -100,6 +102,13 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
<p className={styles.pageSubtitle}>Verwalten Sie alle Mandanten im System</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => navigate('/admin/user-mandates')}
|
||||
>
|
||||
<FaUsers /> Benutzer-Zuweisungen
|
||||
</button>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaUsers, FaKey, FaEnvelopeOpenText } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync, FaUsers, FaKey, FaEnvelopeOpenText, FaUserShield } from 'react-icons/fa';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
interface User {
|
||||
|
|
@ -145,6 +145,13 @@ export const AdminUsersPage: React.FC = () => {
|
|||
<p className={styles.pageSubtitle}>Verwalten Sie alle Benutzer im System</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => navigate('/admin/user-access-overview')}
|
||||
>
|
||||
<FaUserShield /> Zugriffsübersicht
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
|
|
|
|||
269
src/pages/workflows/AutomationTemplatesPage.tsx
Normal file
269
src/pages/workflows/AutomationTemplatesPage.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
/**
|
||||
* AutomationTemplatesPage
|
||||
*
|
||||
* Page for managing automation templates (CRUD).
|
||||
* Uses FormGeneratorTable for listing and FormGeneratorForm for editing.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useAutomationTemplates, type AutomationTemplate } from '../../hooks/useAutomations';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaSync, FaPlus, FaTimes, FaFileAlt } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import styles from '../admin/Admin.module.css';
|
||||
|
||||
export const AutomationTemplatesPage: React.FC = () => {
|
||||
const {
|
||||
templates,
|
||||
attributes,
|
||||
loading,
|
||||
error,
|
||||
permissions,
|
||||
refetch,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
getTemplate,
|
||||
} = useAutomationTemplates();
|
||||
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
// Modal states
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<AutomationTemplate | null>(null);
|
||||
|
||||
// Initial fetch
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
// Check permissions
|
||||
const canCreate = permissions?.create !== 'n';
|
||||
const canUpdate = permissions?.update !== 'n';
|
||||
const canDelete = permissions?.delete !== 'n';
|
||||
|
||||
// Table columns - FormGeneratorTable auto-renders TextMultilingual in user language
|
||||
const columns = useMemo(() => [
|
||||
{ key: 'label', label: 'Label', type: 'string' as const, sortable: true, searchable: true, width: 200 },
|
||||
{ key: 'overview', label: 'Beschreibung', type: 'string' as const, width: 300 },
|
||||
{ key: '_createdByUserName', label: 'Erstellt von', type: 'string' as const, width: 150 },
|
||||
], []);
|
||||
|
||||
// Form attributes for create/edit
|
||||
const formAttributes = useMemo(() => {
|
||||
// Filter to editable fields
|
||||
const editableFields = ['label', 'overview', 'template'];
|
||||
return (attributes || []).filter(attr => editableFields.includes(attr.name));
|
||||
}, [attributes]);
|
||||
|
||||
// Handle edit click
|
||||
const handleEditClick = async (template: AutomationTemplate) => {
|
||||
// Fetch full template data
|
||||
const fullTemplate = await getTemplate(template.id);
|
||||
if (fullTemplate) {
|
||||
setEditingTemplate(fullTemplate);
|
||||
} else {
|
||||
setEditingTemplate(template);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle create submit
|
||||
const handleCreateSubmit = async (data: Partial<AutomationTemplate>) => {
|
||||
try {
|
||||
await createTemplate(data as any);
|
||||
setShowCreateModal(false);
|
||||
showSuccess('Vorlage erstellt');
|
||||
await refetch();
|
||||
} catch (err: any) {
|
||||
showError(`Fehler: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit submit
|
||||
const handleEditSubmit = async (data: Partial<AutomationTemplate>) => {
|
||||
if (!editingTemplate) return;
|
||||
try {
|
||||
await updateTemplate(editingTemplate.id, data);
|
||||
setEditingTemplate(null);
|
||||
showSuccess('Vorlage aktualisiert');
|
||||
await refetch();
|
||||
} catch (err: any) {
|
||||
showError(`Fehler: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete by ID (used by DeleteActionButton via hookData)
|
||||
const handleDelete = async (templateId: string): Promise<boolean> => {
|
||||
try {
|
||||
await deleteTemplate(templateId);
|
||||
showSuccess('Vorlage gelöscht');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
showError(`Fehler: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>Fehler beim Laden der Vorlagen: {error}</p>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||||
<FaSync /> Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Automation-Vorlagen</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie Ihre Workflow-Vorlagen</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||||
</button>
|
||||
{canCreate && (
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
<FaPlus /> Neue Vorlage
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableContainer}>
|
||||
{loading && (!templates || templates.length === 0) ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Vorlagen...</span>
|
||||
</div>
|
||||
) : !templates || templates.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaFileAlt className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Vorlagen vorhanden</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Erstellen Sie eine neue Vorlage für Ihre Workflows.
|
||||
</p>
|
||||
{canCreate && (
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
<FaPlus /> Vorlage erstellen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorTable
|
||||
data={templates as any[]}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
selectable={false}
|
||||
actionButtons={[
|
||||
...(canUpdate ? [{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Bearbeiten',
|
||||
}] : []),
|
||||
...(canDelete ? [{
|
||||
type: 'delete' as const,
|
||||
title: 'Löschen',
|
||||
}] : []),
|
||||
]}
|
||||
onDelete={(template) => handleDelete(template.id)}
|
||||
hookData={{
|
||||
refetch,
|
||||
handleDelete,
|
||||
attributes,
|
||||
}}
|
||||
emptyMessage="Keine Vorlagen gefunden"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create Modal */}
|
||||
{showCreateModal && (
|
||||
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Neue Vorlage</h2>
|
||||
<button className={styles.modalClose} onClick={() => setShowCreateModal(false)}>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{formAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={formAttributes}
|
||||
mode="create"
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText="Erstellen"
|
||||
cancelButtonText="Abbrechen"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editingTemplate && (
|
||||
<div className={styles.modalOverlay} onClick={() => setEditingTemplate(null)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Vorlage bearbeiten</h2>
|
||||
<button className={styles.modalClose} onClick={() => setEditingTemplate(null)}>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{formAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={formAttributes}
|
||||
data={editingTemplate}
|
||||
mode="edit"
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={() => setEditingTemplate(null)}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutomationTemplatesPage;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { PlaygroundPage } from './PlaygroundPage';
|
||||
export { WorkflowsPage } from './WorkflowsPage';
|
||||
export { AutomationsPage } from './AutomationsPage';
|
||||
export { AutomationTemplatesPage } from './AutomationTemplatesPage';
|
||||
|
|
|
|||
Loading…
Reference in a new issue