fixes from demo1: compliance ui fgtable issues, nodes vertical, nodes editting logic to edit in all editors of a mmandate based on highest level of role
This commit is contained in:
parent
851b509f9e
commit
92f293825f
6 changed files with 212 additions and 52 deletions
|
|
@ -31,7 +31,7 @@ import {
|
|||
type AutoVersion,
|
||||
type AutoTemplateScope,
|
||||
} from '../../../api/workflowApi';
|
||||
import { FlowCanvas, type CanvasNode, type CanvasConnection } from './FlowCanvas';
|
||||
import { FlowCanvas, computeAutoLayout, type CanvasNode, type CanvasConnection } from './FlowCanvas';
|
||||
import { NodeConfigPanel } from './NodeConfigPanel';
|
||||
import { NodeSidebar } from './NodeSidebar';
|
||||
import { CanvasHeader } from './CanvasHeader';
|
||||
|
|
@ -587,6 +587,10 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
}
|
||||
}, [request, instanceId]);
|
||||
|
||||
const handleAutoLayout = useCallback(() => {
|
||||
setCanvasNodes((prev) => computeAutoLayout(prev, canvasConnections));
|
||||
}, [canvasConnections]);
|
||||
|
||||
const _sidebarStyle = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]);
|
||||
|
||||
const renderSidebar = () => {
|
||||
|
|
@ -708,6 +712,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
templateSaving={templateSaving}
|
||||
onNewFromTemplate={() => setTemplatePickerOpen(true)}
|
||||
onWorkflowRename={handleWorkflowRename}
|
||||
onAutoLayout={handleAutoLayout}
|
||||
/>
|
||||
<div className={styles.canvasArea} style={{ display: 'flex', flex: 1, minWidth: 0 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown } from 'react-icons/fa';
|
||||
import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown, FaSitemap } from 'react-icons/fa';
|
||||
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
|
|
@ -34,6 +34,7 @@ interface CanvasHeaderProps {
|
|||
templateSaving?: boolean;
|
||||
onNewFromTemplate?: () => void;
|
||||
onWorkflowRename?: (workflowId: string, newName: string) => void;
|
||||
onAutoLayout?: () => void;
|
||||
}
|
||||
|
||||
function _getStatusBadge(t: (key: string) => string): Record<string, { label: string; color: string }> {
|
||||
|
|
@ -68,6 +69,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
templateSaving,
|
||||
onNewFromTemplate,
|
||||
onWorkflowRename,
|
||||
onAutoLayout,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const statusBadge = _getStatusBadge(t);
|
||||
|
|
@ -216,6 +218,19 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
{saving ? <FaSpinner className={styles.spinner} /> : t('Speichern')}
|
||||
</button>
|
||||
|
||||
{onAutoLayout && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.retryButton}
|
||||
onClick={onAutoLayout}
|
||||
disabled={!hasNodes}
|
||||
title={t('Knoten automatisch anordnen')}
|
||||
>
|
||||
<FaSitemap style={{ marginRight: '0.4rem' }} />
|
||||
{t('Anordnen')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Save as template */}
|
||||
{currentWorkflowId && onSaveAsTemplate && (
|
||||
<div ref={templateMenuRef} style={{ position: 'relative', display: 'inline-block' }}>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,75 @@ const NODE_WIDTH = 200;
|
|||
const NODE_HEIGHT = 72;
|
||||
const HANDLE_SIZE = 12;
|
||||
const HANDLE_OFFSET = HANDLE_SIZE / 2;
|
||||
const LAYOUT_V_GAP = 80;
|
||||
const LAYOUT_H_GAP = 60;
|
||||
|
||||
/**
|
||||
* Topological-sort based auto-layout: arranges nodes top-to-bottom in layers.
|
||||
* Disconnected nodes are appended as extra roots.
|
||||
*/
|
||||
export function computeAutoLayout(
|
||||
nodes: CanvasNode[],
|
||||
connections: CanvasConnection[],
|
||||
): CanvasNode[] {
|
||||
if (nodes.length === 0) return nodes;
|
||||
|
||||
const inDegree = new Map<string, number>();
|
||||
const children = new Map<string, string[]>();
|
||||
for (const n of nodes) {
|
||||
inDegree.set(n.id, 0);
|
||||
children.set(n.id, []);
|
||||
}
|
||||
for (const c of connections) {
|
||||
inDegree.set(c.targetId, (inDegree.get(c.targetId) ?? 0) + 1);
|
||||
children.get(c.sourceId)?.push(c.targetId);
|
||||
}
|
||||
|
||||
const layers: string[][] = [];
|
||||
const layerOf = new Map<string, number>();
|
||||
const queue: string[] = [];
|
||||
for (const n of nodes) {
|
||||
if ((inDegree.get(n.id) ?? 0) === 0) queue.push(n.id);
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const batch: string[] = [...queue];
|
||||
queue.length = 0;
|
||||
const layerIdx = layers.length;
|
||||
layers.push(batch);
|
||||
for (const id of batch) {
|
||||
layerOf.set(id, layerIdx);
|
||||
for (const childId of children.get(id) ?? []) {
|
||||
const deg = (inDegree.get(childId) ?? 1) - 1;
|
||||
inDegree.set(childId, deg);
|
||||
if (deg === 0) queue.push(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const placed = new Set(layerOf.keys());
|
||||
for (const n of nodes) {
|
||||
if (!placed.has(n.id)) {
|
||||
const layerIdx = layers.length;
|
||||
layers.push([n.id]);
|
||||
layerOf.set(n.id, layerIdx);
|
||||
}
|
||||
}
|
||||
|
||||
const startX = 40;
|
||||
const startY = 40;
|
||||
|
||||
return nodes.map((n) => {
|
||||
const layer = layerOf.get(n.id) ?? 0;
|
||||
const siblings = layers[layer];
|
||||
const idxInLayer = siblings.indexOf(n.id);
|
||||
return {
|
||||
...n,
|
||||
x: startX + idxInLayer * (NODE_WIDTH + LAYOUT_H_GAP),
|
||||
y: startY + layer * (NODE_HEIGHT + LAYOUT_V_GAP),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Soft port compatibility check: returns 'ok' | 'warning' | 'error' */
|
||||
function _checkConnectionCompatibility(
|
||||
|
|
@ -164,26 +233,16 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
|||
|
||||
const w = NODE_WIDTH;
|
||||
const h = NODE_HEIGHT;
|
||||
const centerY = node.y + h / 2;
|
||||
const centerX = node.x + w / 2;
|
||||
|
||||
if (isOutput) {
|
||||
if (ioCount === 1) return { x: node.x + w, y: centerY, side: 'right' };
|
||||
if (ioCount === 2) {
|
||||
return ioIndex === 0
|
||||
? { x: node.x + w, y: node.y + h / 3, side: 'right' }
|
||||
: { x: node.x + w, y: node.y + (2 * h) / 3, side: 'right' };
|
||||
}
|
||||
const step = h / (ioCount + 1);
|
||||
return { x: node.x + w, y: node.y + step * (ioIndex + 1), side: 'right' };
|
||||
if (ioCount === 1) return { x: centerX, y: node.y + h, side: 'bottom' };
|
||||
const step = w / (ioCount + 1);
|
||||
return { x: node.x + step * (ioIndex + 1), y: node.y + h, side: 'bottom' };
|
||||
} else {
|
||||
if (ioCount === 1) return { x: node.x, y: centerY, side: 'left' };
|
||||
if (ioCount === 2) {
|
||||
return ioIndex === 0
|
||||
? { x: node.x, y: node.y + h / 3, side: 'left' }
|
||||
: { x: node.x, y: node.y + (2 * h) / 3, side: 'left' };
|
||||
}
|
||||
const step = h / (ioCount + 1);
|
||||
return { x: node.x, y: node.y + step * (ioIndex + 1), side: 'left' };
|
||||
if (ioCount === 1) return { x: centerX, y: node.y, side: 'top' };
|
||||
const step = w / (ioCount + 1);
|
||||
return { x: node.x + step * (ioIndex + 1), y: node.y, side: 'top' };
|
||||
}
|
||||
},
|
||||
[]
|
||||
|
|
@ -639,8 +698,8 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
|||
if (!srcNode || !tgtNode) return null;
|
||||
const src = getHandlePosition(srcNode, c.sourceHandle);
|
||||
const tgt = getHandlePosition(tgtNode, c.targetHandle);
|
||||
const dx = tgt.x - src.x;
|
||||
const pathD = `M ${src.x} ${src.y} C ${src.x + Math.abs(dx) / 2} ${src.y}, ${tgt.x - Math.abs(dx) / 2} ${tgt.y}, ${tgt.x} ${tgt.y}`;
|
||||
const dy = tgt.y - src.y;
|
||||
const pathD = `M ${src.x} ${src.y} C ${src.x} ${src.y + Math.abs(dy) / 2}, ${tgt.x} ${tgt.y - Math.abs(dy) / 2}, ${tgt.x} ${tgt.y}`;
|
||||
const isSelected = selectedConnectionId === c.id;
|
||||
const isWarning = connectionWarnings[c.id];
|
||||
const strokeColor = isSelected
|
||||
|
|
@ -756,12 +815,12 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
|||
key={index}
|
||||
className={styles.handleWrapper}
|
||||
style={{
|
||||
left: pos.side === 'left' ? -HANDLE_OFFSET : undefined,
|
||||
right: pos.side === 'right' ? -HANDLE_OFFSET : undefined,
|
||||
top: pos.y - node.y - HANDLE_OFFSET,
|
||||
top: pos.side === 'top' ? -HANDLE_OFFSET : undefined,
|
||||
bottom: pos.side === 'bottom' ? -HANDLE_OFFSET : undefined,
|
||||
left: pos.x - node.x - HANDLE_OFFSET,
|
||||
}}
|
||||
>
|
||||
{outputLabel && pos.side === 'right' && (
|
||||
{outputLabel && pos.side === 'bottom' && (
|
||||
<span className={styles.handleLabel}>{outputLabel}</span>
|
||||
)}
|
||||
<div
|
||||
|
|
@ -778,7 +837,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
|||
: undefined)
|
||||
}
|
||||
/>
|
||||
{outputLabel && pos.side === 'left' && (
|
||||
{outputLabel && pos.side === 'top' && (
|
||||
<span className={styles.handleLabel}>{outputLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { formatUnixTimestamp } from '../utils/time';
|
|||
import { updateWorkflow, executeGraph, deleteSystemWorkflow } from '../api/workflowApi';
|
||||
import api from '../api';
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
import { useNavigation, type DynamicBlock } from '../hooks/useNavigation';
|
||||
import styles from './admin/Admin.module.css';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -53,6 +54,7 @@ interface SystemWorkflow {
|
|||
id: string;
|
||||
mandateId: string;
|
||||
featureInstanceId: string;
|
||||
featureCode?: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
isRunning?: boolean;
|
||||
|
|
@ -72,6 +74,43 @@ interface SystemWorkflow {
|
|||
graph?: Record<string, any>;
|
||||
}
|
||||
|
||||
const _FEATURES_WITH_EDITOR = new Set(['graphicalEditor', 'workspace']);
|
||||
|
||||
const _ROLE_PRIORITY: Record<string, number> = { admin: 3, user: 2, viewer: 1 };
|
||||
|
||||
function _bestEditorInstance(
|
||||
dynamicBlock: DynamicBlock | null,
|
||||
mandateId: string,
|
||||
): { instanceId: string; featureCode: string } | null {
|
||||
if (!dynamicBlock) return null;
|
||||
const mandate = dynamicBlock.mandates.find((m) => m.id === mandateId);
|
||||
if (!mandate) return null;
|
||||
|
||||
let best: { instanceId: string; featureCode: string; score: number } | null = null;
|
||||
for (const feat of mandate.features) {
|
||||
for (const inst of feat.instances) {
|
||||
const fc = inst.featureCode
|
||||
|| feat.uiComponent.replace(/^feature\./, '');
|
||||
if (!_FEATURES_WITH_EDITOR.has(fc)) continue;
|
||||
let score = 0;
|
||||
if (inst.isAdmin) {
|
||||
score = 10;
|
||||
} else {
|
||||
for (const v of inst.views) {
|
||||
const key = v.objectKey || '';
|
||||
for (const [suffix, prio] of Object.entries(_ROLE_PRIORITY)) {
|
||||
if (key.endsWith(suffix) && prio > score) score = prio;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!best || score > best.score) {
|
||||
best = { instanceId: inst.id, featureCode: fc, score };
|
||||
}
|
||||
}
|
||||
}
|
||||
return best ? { instanceId: best.instanceId, featureCode: best.featureCode } : null;
|
||||
}
|
||||
|
||||
function _formatTs(ts?: number): string {
|
||||
if (ts == null || ts <= 0) return '—';
|
||||
const sec = ts < 1e12 ? ts : ts / 1000;
|
||||
|
|
@ -664,6 +703,7 @@ const _WorkflowsTab: React.FC = () => {
|
|||
const { request } = useApiRequest();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { prompt: promptInput, PromptDialog } = usePrompt();
|
||||
const { dynamicBlock } = useNavigation();
|
||||
|
||||
const [workflows, setWorkflows] = useState<SystemWorkflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -718,10 +758,19 @@ const _WorkflowsTab: React.FC = () => {
|
|||
}, [hasRunningWorkflows, _load]);
|
||||
|
||||
const _handleEdit = useCallback((row: SystemWorkflow) => {
|
||||
if (!row.mandateId || !row.featureInstanceId) return;
|
||||
const fc = (row as any).featureCode || 'graphicalEditor';
|
||||
if (!row.mandateId) return;
|
||||
const fc = row.featureCode || '';
|
||||
if (_FEATURES_WITH_EDITOR.has(fc)) {
|
||||
navigate(`/mandates/${row.mandateId}/${fc}/${row.featureInstanceId}/editor?workflowId=${row.id}`);
|
||||
}, [navigate]);
|
||||
return;
|
||||
}
|
||||
const editor = _bestEditorInstance(dynamicBlock, row.mandateId);
|
||||
if (!editor) {
|
||||
showError(t('Kein Editor verfügbar für diesen Mandanten'));
|
||||
return;
|
||||
}
|
||||
navigate(`/mandates/${row.mandateId}/${editor.featureCode}/${editor.instanceId}/editor?workflowId=${row.id}`);
|
||||
}, [navigate, showError, t, dynamicBlock]);
|
||||
|
||||
const _handleDelete = useCallback(async (workflowId: string): Promise<boolean> => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||
const [mandatesLoading, setMandatesLoading] = useState(true);
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<TabId>('ai-log');
|
||||
const [activeTab, setActiveTab] = useState<TabId>('audit-log');
|
||||
|
||||
// ── Tab A: AI-Log state ──
|
||||
const [aiEntries, setAiEntries] = useState<any[]>([]);
|
||||
|
|
@ -193,8 +193,13 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
const pageSize = paginationParams?.pageSize ?? _AI_LOG_PAGE_SIZE;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const params: any = { limit: pageSize, offset };
|
||||
if (paginationParams?.sort?.length) params.sort = JSON.stringify(paginationParams.sort);
|
||||
if (paginationParams?.filters && Object.keys(paginationParams.filters).length) params.filters = JSON.stringify(paginationParams.filters);
|
||||
if (paginationParams?.search) params.search = paginationParams.search;
|
||||
|
||||
const { data } = await api.get('/api/audit/ai-log', {
|
||||
params: { limit: pageSize, offset },
|
||||
params,
|
||||
headers: _mandateHeaders(),
|
||||
});
|
||||
const items: any[] = data?.items ?? [];
|
||||
|
|
@ -220,8 +225,13 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
const pageSize = paginationParams?.pageSize ?? _AUDIT_LOG_PAGE_SIZE;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const params: any = { limit: pageSize, offset };
|
||||
if (paginationParams?.sort?.length) params.sort = JSON.stringify(paginationParams.sort);
|
||||
if (paginationParams?.filters && Object.keys(paginationParams.filters).length) params.filters = JSON.stringify(paginationParams.filters);
|
||||
if (paginationParams?.search) params.search = paginationParams.search;
|
||||
|
||||
const { data } = await api.get('/api/audit/log', {
|
||||
params: { limit: pageSize, offset },
|
||||
params,
|
||||
headers: _mandateHeaders(),
|
||||
});
|
||||
const items: any[] = data?.items ?? [];
|
||||
|
|
@ -262,8 +272,13 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
const pageSize = paginationParams?.pageSize ?? _NEUT_PAGE_SIZE;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const neutParams: any = { limit: pageSize, offset };
|
||||
if (paginationParams?.sort?.length) neutParams.sort = JSON.stringify(paginationParams.sort);
|
||||
if (paginationParams?.filters && Object.keys(paginationParams.filters).length) neutParams.filters = JSON.stringify(paginationParams.filters);
|
||||
if (paginationParams?.search) neutParams.search = paginationParams.search;
|
||||
|
||||
const { data } = await api.get('/api/audit/neutralization-mappings', {
|
||||
params: { limit: pageSize, offset },
|
||||
params: neutParams,
|
||||
headers: _mandateHeaders(),
|
||||
});
|
||||
const items: any[] = data?.items ?? [];
|
||||
|
|
@ -410,8 +425,8 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
formatter: (val: any, row: any) => val || (row?.userId ? row.userId.slice(0, 8) : '–'),
|
||||
},
|
||||
{
|
||||
key: 'featureCode', label: t('Feature'), type: 'text' as any, sortable: true, filterable: true, width: 130,
|
||||
formatter: (val: any, row: any) => row?.instanceLabel || val || '–',
|
||||
key: 'instanceLabel', label: t('Feature-Instanz'), type: 'text' as any, sortable: true, filterable: true, width: 160,
|
||||
formatter: (val: any, row: any) => val || row?.featureCode || '–',
|
||||
},
|
||||
{ key: 'aiModel', label: t('AI-Modell'), type: 'text' as any, sortable: true, filterable: true, width: 160 },
|
||||
{
|
||||
|
|
@ -467,12 +482,12 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
{ key: 'originalText', label: t('Originaltext'), type: 'text' as any, sortable: true, searchable: true, width: 240 },
|
||||
{ key: 'patternType', label: t('Kategorie'), type: 'text' as any, sortable: true, filterable: true, width: 120 },
|
||||
{
|
||||
key: 'userId', label: t('Benutzer'), type: 'text' as any, sortable: true, filterable: true, width: 140,
|
||||
formatter: (val: any) => val ? String(val).slice(0, 8) + '…' : '–',
|
||||
key: 'username', label: t('Benutzer'), type: 'text' as any, sortable: true, filterable: true, width: 140,
|
||||
formatter: (val: any, row: any) => val || (row?.userId ? String(row.userId).slice(0, 8) + '…' : '–'),
|
||||
},
|
||||
{
|
||||
key: 'featureInstanceId', label: t('Feature-Instanz'), type: 'text' as any, sortable: true, filterable: true, width: 160,
|
||||
formatter: (val: any) => val || '–',
|
||||
key: 'instanceLabel', label: t('Feature-Instanz'), type: 'text' as any, sortable: true, filterable: true, width: 160,
|
||||
formatter: (val: any, row: any) => val || (row?.featureInstanceId ? String(row.featureInstanceId).slice(0, 8) + '…' : '–'),
|
||||
},
|
||||
{
|
||||
key: 'fileId', label: t('Datei'), type: 'text' as any, sortable: true, width: 140,
|
||||
|
|
@ -480,26 +495,46 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
},
|
||||
], [t]);
|
||||
|
||||
// ── fetchFilterValues for autofilter dropdowns ──
|
||||
|
||||
const _makeFetchFilterValues = useCallback(
|
||||
(endpoint: string) => async (columnKey: string, crossFilters?: Record<string, any>) => {
|
||||
if (!selectedMandateId) return [];
|
||||
try {
|
||||
const params: any = { mode: 'filterValues', column: columnKey };
|
||||
if (crossFilters && Object.keys(crossFilters).length) {
|
||||
params.filters = JSON.stringify(crossFilters);
|
||||
}
|
||||
const { data } = await api.get(endpoint, { params, headers: _mandateHeaders() });
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch { return []; }
|
||||
},
|
||||
[selectedMandateId], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
// ── hookData for FormGeneratorTable ──
|
||||
|
||||
const aiLogHookData = useMemo(() => ({
|
||||
refetch: _loadAiLog,
|
||||
pagination: aiPagination,
|
||||
}), [_loadAiLog, aiPagination]);
|
||||
fetchFilterValues: _makeFetchFilterValues('/api/audit/ai-log'),
|
||||
}), [_loadAiLog, aiPagination, _makeFetchFilterValues]);
|
||||
|
||||
const auditLogHookData = useMemo(() => ({
|
||||
refetch: _loadAuditLog,
|
||||
pagination: auditPagination,
|
||||
}), [_loadAuditLog, auditPagination]);
|
||||
fetchFilterValues: _makeFetchFilterValues('/api/audit/log'),
|
||||
}), [_loadAuditLog, auditPagination, _makeFetchFilterValues]);
|
||||
|
||||
const neutHookData = useMemo(() => ({
|
||||
refetch: _loadNeutMappings,
|
||||
pagination: neutPagination,
|
||||
}), [_loadNeutMappings, neutPagination]);
|
||||
fetchFilterValues: _makeFetchFilterValues('/api/audit/neutralization-mappings'),
|
||||
}), [_loadNeutMappings, neutPagination, _makeFetchFilterValues]);
|
||||
|
||||
// ── Render ──
|
||||
|
||||
const _tabs: TabId[] = ['ai-log', 'audit-log', 'stats', 'neutralization'];
|
||||
const _tabs: TabId[] = ['audit-log', 'ai-log', 'neutralization', 'stats'];
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
|
|
|
|||
|
|
@ -17,15 +17,14 @@ import styles from './Settings.module.css';
|
|||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
type SettingsTab = 'profile' | 'appearance' | 'voice' | 'neutralization' | 'privacy';
|
||||
type SettingsTab = 'profile' | 'appearance' | 'voice' | 'privacy';
|
||||
|
||||
function _getTabs(t: (key: string) => string): { key: SettingsTab; label: string }[] {
|
||||
return [
|
||||
{ key: 'profile', label: t('Tab Profil') },
|
||||
{ key: 'appearance', label: t('Tab Darstellung') },
|
||||
{ key: 'voice', label: t('Tab Stimme & Sprache') },
|
||||
{ key: 'neutralization', label: t('Tab Neutralisierung') },
|
||||
{ key: 'privacy', label: t('Tab Datenschutz') },
|
||||
{ key: 'profile', label: t('Profil') },
|
||||
{ key: 'appearance', label: t('Darstellung') },
|
||||
{ key: 'voice', label: t('Stimme & Sprache') },
|
||||
{ key: 'privacy', label: t('Datenschutz') },
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -563,8 +562,6 @@ export const SettingsPage: React.FC = () => {
|
|||
|
||||
{activeTab === 'voice' && <VoiceSettingsTab />}
|
||||
|
||||
{activeTab === 'neutralization' && <NeutralizationMappingsTab />}
|
||||
|
||||
{activeTab === 'privacy' && (
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>{t('Datenschutz')}</h2>
|
||||
|
|
|
|||
Loading…
Reference in a new issue