cleanup internal marked exports

This commit is contained in:
ValueOn AG 2026-04-26 08:31:31 +02:00
parent e09ed758ff
commit c47dc67a84
26 changed files with 1246 additions and 1031 deletions

View file

@ -256,6 +256,225 @@
background: var(--bg-primary, #fff); background: var(--bg-primary, #fff);
} }
/* Toolbar: context (load + name) is fluid with ellipsis; actions stay right-aligned. */
.canvasHeaderRow {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
width: 100%;
}
@media (max-width: 900px) {
.canvasHeaderRow {
grid-template-columns: 1fr;
}
}
.canvasHeaderContext {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
flex: 1;
}
/* Closed <select> width must not follow the longest option label. */
.canvasHeaderWorkflowSelect {
flex: 0 0 auto;
width: 12.5rem;
max-width: 100%;
padding: 0.4rem 0.5rem;
min-height: 2rem;
font-size: 0.85rem;
border: 1px solid var(--border-color, #ccc);
border-radius: 6px;
background: var(--bg-primary, #fff);
color: var(--text-primary, #333);
}
.canvasHeaderTitleBlock {
flex: 1 1 8rem;
min-width: 0;
display: flex;
align-items: center;
gap: 0.25rem;
}
.canvasHeaderTitle,
.canvasHeaderTitle input {
margin: 0;
min-width: 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
}
.canvasHeaderTitle {
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.canvasHeaderTitleMuted {
font-style: italic;
font-weight: 500;
opacity: 0.65;
color: var(--text-secondary, #666);
}
.canvasHeaderTitle input {
width: 100%;
max-width: 100%;
padding: 0.25rem 0.4rem;
border: 1px solid var(--primary-color, #007bff);
border-radius: 4px;
outline: none;
background: var(--bg-primary, #fff);
box-sizing: border-box;
}
.canvasHeaderActionPanel {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.4rem;
padding: 0.35rem 0.5rem;
border-radius: 8px;
border: 1px solid var(--border-color, #e0e0e0);
background: var(--bg-secondary, #f8f9fa);
flex: 0 1 auto;
max-width: 100%;
}
/* .retryButton sets margin-top for legacy error stacks — not wanted in the toolbar. */
.canvasHeaderActionPanel button {
margin-top: 0;
}
/* Run label switches between "Ausführen", "Ausführen…", "Pflicht-Felder fehlen" — reserve space. */
.canvasHeaderRunButton {
min-width: 12.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
}
@media (max-width: 900px) {
.canvasHeaderActionPanel {
justify-content: flex-start;
}
}
.canvasHeaderVersionRow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color, #e8e8e8);
width: 100%;
}
.canvasHeaderVersionRow button {
margin-top: 0;
}
.canvasHeaderVersionLabel {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary, #666);
flex: 0 0 auto;
}
.canvasHeaderVersionSelect {
width: 11rem;
max-width: 100%;
padding: 0.3rem 0.45rem;
font-size: 0.85rem;
min-height: 1.9rem;
border: 1px solid var(--border-color, #ccc);
border-radius: 4px;
background: var(--bg-primary, #fff);
color: var(--text-primary, #333);
}
.canvasHeaderSysadmin {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.75rem;
color: var(--text-secondary, #666);
padding: 0.2rem 0.45rem;
border: 1px dashed var(--border-color, #ccc);
border-radius: 4px;
cursor: pointer;
user-select: none;
white-space: nowrap;
flex: 0 0 auto;
}
.canvasHeaderNewSplit {
position: relative;
display: inline-flex;
flex: 0 0 auto;
}
.canvasHeaderSplitPair {
display: flex;
flex: 0 0 auto;
}
.canvasHeaderNewSplitMain {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.canvasHeaderNewSplitMenu {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding-left: 0.25rem;
padding-right: 0.4rem;
border-left: 1px solid rgba(0, 0, 0, 0.12);
}
.canvasHeaderMenuDropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 100;
background: var(--bg-primary, #fff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
min-width: 11rem;
margin-top: 0.25rem;
}
.canvasHeaderMenuItem {
display: block;
width: 100%;
text-align: left;
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
cursor: pointer;
font-size: 0.85rem;
color: var(--text-primary, #333);
}
.canvasHeaderMenuItem:hover {
background: var(--bg-hover, #e9ecef);
}
.canvasHeaderMenuItem + .canvasHeaderMenuItem {
border-top: 1px solid var(--border-color, #e0e0e0);
}
.canvasTitle { .canvasTitle {
margin: 0; margin: 0;
font-size: 0.875rem; font-size: 0.875rem;
@ -507,20 +726,32 @@
cursor: copy; cursor: copy;
} }
/* Node Config Panel */ /* Node Config Panel
* Fixed-width side panel. The `box-sizing: border-box` + `overflow-x: hidden`
* pair acts as a safety net so long unbreakable strings (type names like
* `List[ActionDocument]`, hashed IDs, refs like ` node.path field`) can
* never push content out of the panel frame. Children rely on this; e.g.
* `RequiredAttributePicker` lays out label/badge so the badge wraps below
* a long label rather than escaping to the right.
*/
.nodeConfigPanel { .nodeConfigPanel {
padding: 1rem; padding: 1rem;
background: var(--bg-primary, #fff); background: var(--bg-primary, #fff);
border-left: 1px solid var(--border-color, #e0e0e0); border-left: 1px solid var(--border-color, #e0e0e0);
width: 280px; width: 280px;
flex-shrink: 0; flex-shrink: 0;
box-sizing: border-box;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
min-width: 0; min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
} }
.nodeConfigPanel h4 { .nodeConfigPanel h4 {
margin: 0 0 0.75rem 0; margin: 0 0 0.75rem 0;
font-size: 0.9rem; font-size: 0.9rem;
overflow-wrap: anywhere;
} }
.nodeConfigNameRow { .nodeConfigNameRow {
@ -547,6 +778,8 @@
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary, #666); color: var(--text-secondary, #666);
line-height: 1.4; line-height: 1.4;
overflow-wrap: anywhere;
word-break: break-word;
} }
.nodeConfigPanel label { .nodeConfigPanel label {
@ -572,7 +805,8 @@
min-height: 60px; min-height: 60px;
} }
/* Kein Primär-Button-Stil für Zeitplan-Karten / Wochentage / Monat-Jahr-Chips */ /* Kein Primär-Button-Stil für Zeitplan-Karten / Wochentage / Monat-Jahr-Chips
(DataPicker-Dialog wird per createPortal an document.body gehangen nicht hier). */
.nodeConfigPanel .nodeConfigPanel
button:not(.scheduleModeCard):not(.scheduleDayOn):not(.scheduleDayOff):not(.scheduleSubModeBtn) { button:not(.scheduleModeCard):not(.scheduleDayOn):not(.scheduleDayOff):not(.scheduleSubModeBtn) {
margin-top: 0.5rem; margin-top: 0.5rem;
@ -1284,53 +1518,112 @@
min-width: 0; min-width: 0;
} }
/* Data Picker */ /* Data Picker rendered with createPortal(document.body) so it is not affected
by .nodeConfigPanels generic CTA `button` styles. */
.dataPickerOverlay { .dataPickerOverlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.35); background: rgba(0, 0, 0, 0.4);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 11000;
padding: 1rem;
box-sizing: border-box;
} }
.dataPickerModal { .dataPickerModal {
background: var(--bg-primary, #fff); background: var(--bg-primary, #fff);
border-radius: 8px; color: var(--text-primary, #1a1a1a);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); border-radius: 10px;
max-width: 420px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
max-height: 80vh; border: 1px solid var(--border-color, #e0e0e0);
max-width: min(420px, 100vw - 2rem);
width: 100%;
max-height: min(80vh, 640px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0;
} }
.dataPickerHeader { .dataPickerHeader {
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
padding: 1rem 1.25rem; gap: 0.75rem;
padding: 1rem 1.15rem;
border-bottom: 1px solid var(--border-color, #e0e0e0); border-bottom: 1px solid var(--border-color, #e0e0e0);
flex-shrink: 0;
}
.dataPickerHeaderControls {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: flex-end;
} }
.dataPickerTitle { .dataPickerTitle {
margin: 0; margin: 0;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1a1a);
line-height: 1.35;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.4rem;
min-width: 0;
}
.dataPickerTypeBadge {
display: inline-block;
font-size: 0.7rem;
font-weight: 400;
font-family: ui-monospace, 'Cascadia Code', monospace;
color: var(--text-secondary, #666);
background: var(--bg-secondary, #f0f0f0);
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
padding: 0.1rem 0.45rem;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dataPickerStrictLabel {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
color: var(--text-secondary, #666);
user-select: none;
} }
.dataPickerClose { .dataPickerClose {
background: none; display: inline-flex;
border: none; align-items: center;
font-size: 1.5rem; justify-content: center;
cursor: pointer; width: 2rem;
color: var(--text-secondary, #666); height: 2rem;
padding: 0 0.25rem; flex-shrink: 0;
background: var(--bg-secondary, #f5f5f5);
border: 1px solid var(--border-color, #d0d0d0);
border-radius: 6px;
font-size: 1.25rem;
line-height: 1; line-height: 1;
cursor: pointer;
color: var(--text-primary, #333);
} }
.dataPickerClose:hover { .dataPickerClose:hover {
color: var(--text-primary, #333); background: var(--bg-hover, #e9ecef);
color: var(--text-primary, #1a1a1a);
border-color: var(--border-color, #b8b8b8);
} }
.dataPickerBody { .dataPickerBody {
@ -1345,24 +1638,35 @@
} }
.dataPickerNodeSection { .dataPickerNodeSection {
margin-bottom: 0.75rem; margin-bottom: 0.5rem;
} }
/* Expandable source row: neutral “list row”, not a primary CTA. */
.dataPickerNodeHeader { .dataPickerNodeHeader {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 0.5rem 0; box-sizing: border-box;
background: none; padding: 0.5rem 0.6rem;
border: none; background: var(--bg-secondary, #f4f5f7);
border: 1px solid var(--border-color, #dde1e5);
border-radius: 6px;
cursor: pointer; cursor: pointer;
font-size: 0.875rem; font-size: 0.85rem;
text-align: left; text-align: left;
color: var(--text-primary, #1a1a1a);
margin: 0;
transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
} }
.dataPickerNodeHeader:hover { .dataPickerNodeHeader:hover {
background: var(--bg-hover, #f5f5f5); background: var(--bg-hover, #e9ebef);
border-radius: 4px; border-color: var(--border-color, #c8cfd6);
}
.dataPickerNodeHeader:focus-visible {
outline: 2px solid var(--primary-color, #4a6fa5);
outline-offset: 1px;
} }
.dataPickerExpandIcon { .dataPickerExpandIcon {
@ -1401,6 +1705,43 @@
border-color: var(--primary-color, #007bff); border-color: var(--primary-color, #007bff);
} }
/* Hover safety net: every nested span in a leaf inherits the white text so
* type-hints and meta info stay readable on the blue hover background. */
.dataPickerLeaf:hover * {
color: inherit;
}
/* Inline type-hint after a leaf label, e.g. "documents (List[ActionDocument])". */
.dataPickerLeafType {
color: var(--text-secondary, #666);
font-size: 10px;
margin-left: 4px;
}
/* Schema-name hint on the node-section header row. */
.dataPickerNodeSchemaHint {
color: var(--text-secondary, #666);
font-size: 10px;
margin-left: 4px;
}
/* "iterieren" affordance visually distinct (subtle accent), readable on
* the picker's white background and on the leaf's blue hover background. */
.dataPickerIterateBtn {
font-size: 10px;
padding: 2px 6px;
background: var(--bg-secondary, #f5f7fa);
color: var(--primary-color, #007bff);
border: 1px solid var(--border-color, #e0e0e0);
white-space: nowrap;
}
.dataPickerIterateBtn:hover {
background: var(--primary-color, #007bff);
color: #fff;
border-color: var(--primary-color, #007bff);
}
/* Dynamic Value Field */ /* Dynamic Value Field */
.dynamicValueField { .dynamicValueField {
display: flex; display: flex;

View file

@ -58,6 +58,7 @@ import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
import styles from './Automation2FlowEditor.module.css'; import styles from './Automation2FlowEditor.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
import { useToast } from '../../../contexts/ToastContext';
const LOG = '[Automation2]'; const LOG = '[Automation2]';
@ -90,6 +91,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
onSourcesChanged, onSourcesChanged,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const { showError } = useToast();
const { request } = useApiRequest(); const { request } = useApiRequest();
const { prompt: promptInput, PromptDialog } = usePrompt(); const { prompt: promptInput, PromptDialog } = usePrompt();
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]); const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
@ -137,6 +139,15 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
const [sidebarWidth, setSidebarWidth] = useState(() => { const [sidebarWidth, setSidebarWidth] = useState(() => {
try { const v = parseInt(localStorage.getItem('flowEditor.sidebarWidth') ?? ''); return v >= 200 && v <= 500 ? v : 280; } catch { return 280; } try { const v = parseInt(localStorage.getItem('flowEditor.sidebarWidth') ?? ''); return v >= 200 && v <= 500 ? v : 280; } catch { return 280; }
}); });
// Verbose schema toggle: shows the static type-reference block (input/output
// schema) and parameter type-badges in NodeConfigPanel. Only the
// CanvasHeader exposes the toggle (sysadmin-only); persisted to localStorage.
const [verboseSchema, setVerboseSchema] = useState(() => {
try { return localStorage.getItem('flowEditor.verboseSchema') === '1'; } catch { return false; }
});
useEffect(() => {
try { localStorage.setItem('flowEditor.verboseSchema', verboseSchema ? '1' : '0'); } catch { /* ignore */ }
}, [verboseSchema]);
const resizingRef = useRef<{ target: 'left' | 'right'; startX: number; startW: number } | null>(null); const resizingRef = useRef<{ target: 'left' | 'right'; startX: number; startW: number } | null>(null);
useEffect(() => { useEffect(() => {
@ -655,9 +666,11 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
await updateWorkflow(request, instanceId, workflowId, { label: newName }); await updateWorkflow(request, instanceId, workflowId, { label: newName });
setWorkflows((prev) => prev.map((w) => w.id === workflowId ? { ...w, label: newName } : w)); setWorkflows((prev) => prev.map((w) => w.id === workflowId ? { ...w, label: newName } : w));
} catch (e: unknown) { } catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
console.error(`${LOG} rename failed`, e); console.error(`${LOG} rename failed`, e);
showError(t('Workflow umbenennen fehlgeschlagen: {msg}', { msg }));
} }
}, [request, instanceId]); }, [request, instanceId, showError, t]);
const handleAutoLayout = useCallback(() => { const handleAutoLayout = useCallback(() => {
setCanvasNodes((prev) => computeAutoLayout(prev, canvasConnections)); setCanvasNodes((prev) => computeAutoLayout(prev, canvasConnections));
@ -821,6 +834,8 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
onNewFromTemplate={() => setTemplatePickerOpen(true)} onNewFromTemplate={() => setTemplatePickerOpen(true)}
onWorkflowRename={handleWorkflowRename} onWorkflowRename={handleWorkflowRename}
onAutoLayout={handleAutoLayout} onAutoLayout={handleAutoLayout}
verboseSchema={verboseSchema}
onVerboseSchemaChange={setVerboseSchema}
/> />
<div className={styles.canvasArea} style={{ display: 'flex', flex: 1, minWidth: 0 }}> <div className={styles.canvasArea} style={{ display: 'flex', flex: 1, minWidth: 0 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
@ -875,6 +890,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
onNodeUpdate={handleNodeUpdate} onNodeUpdate={handleNodeUpdate}
instanceId={instanceId} instanceId={instanceId}
request={request} request={request}
verboseSchema={verboseSchema}
/> />
</Automation2DataFlowProvider> </Automation2DataFlowProvider>
)} )}

View file

@ -8,6 +8,7 @@ import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTempla
import styles from './Automation2FlowEditor.module.css'; import styles from './Automation2FlowEditor.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
import { getUserDataCache } from '../../../utils/userCache';
interface CanvasHeaderProps { interface CanvasHeaderProps {
workflows: Automation2Workflow[]; workflows: Automation2Workflow[];
@ -40,6 +41,10 @@ interface CanvasHeaderProps {
onNewFromTemplate?: () => void; onNewFromTemplate?: () => void;
onWorkflowRename?: (workflowId: string, newName: string) => void; onWorkflowRename?: (workflowId: string, newName: string) => void;
onAutoLayout?: () => void; onAutoLayout?: () => void;
/** Sysadmin-only: when true, NodeConfigPanel renders the static
* "Schema (Typ-Referenz)" block and per-parameter type-badges. */
verboseSchema?: boolean;
onVerboseSchemaChange?: (next: boolean) => void;
} }
function _getStatusBadge(t: (key: string) => string): Record<string, { label: string; color: string }> { function _getStatusBadge(t: (key: string) => string): Record<string, { label: string; color: string }> {
@ -77,8 +82,11 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
onNewFromTemplate, onNewFromTemplate,
onWorkflowRename, onWorkflowRename,
onAutoLayout, onAutoLayout,
verboseSchema,
onVerboseSchemaChange,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const _isSysAdmin = getUserDataCache()?.isSysAdmin === true;
const statusBadge = _getStatusBadge(t); const statusBadge = _getStatusBadge(t);
const currentVersion = versions?.find((v) => v.id === currentVersionId); const currentVersion = versions?.find((v) => v.id === currentVersionId);
const currentStatus = currentVersion?.status || 'draft'; const currentStatus = currentVersion?.status || 'draft';
@ -137,35 +145,59 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
[t] [t]
); );
const _titleHint =
onWorkflowRename && currentWorkflow
? `${currentWorkflow.label}${t('Klicken zum Umbenennen')}`
: currentWorkflow?.label;
return ( return (
<div className={styles.canvasHeader}> <div className={styles.canvasHeader}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}> <div className={styles.canvasHeaderRow}>
{/* Workflow name: inline editable */} <div className={styles.canvasHeaderContext}>
<select
className={styles.canvasHeaderWorkflowSelect}
value={currentWorkflowId ?? ''}
onChange={(e) => {
const id = e.target.value ? e.target.value : null;
onWorkflowSelect(id);
}}
aria-label={t('Workflow laden')}
title={t('Workflow laden')}
>
<option value="">{t('Workflow laden')}</option>
{workflows.map((w) => (
<option key={w.id} value={w.id}>
{w.label}
</option>
))}
</select>
<div className={styles.canvasHeaderTitleBlock}>
{currentWorkflowId && currentWorkflow ? ( {currentWorkflowId && currentWorkflow ? (
editingName ? ( editingName ? (
<input <input
ref={nameInputRef} ref={nameInputRef}
className={styles.canvasHeaderTitle}
value={nameValue} value={nameValue}
onChange={(e) => setNameValue(e.target.value)} onChange={(e) => setNameValue(e.target.value)}
onBlur={_commitNameEdit} onBlur={_commitNameEdit}
onKeyDown={(e) => { if (e.key === 'Enter') _commitNameEdit(); if (e.key === 'Escape') setEditingName(false); }} onKeyDown={(e) => { if (e.key === 'Enter') _commitNameEdit(); if (e.key === 'Escape') setEditingName(false); }}
style={{ padding: '0.25rem 0.4rem', fontSize: '0.95rem', fontWeight: 600, border: '1px solid var(--primary-color, #007bff)', borderRadius: 4, outline: 'none', minWidth: 140, maxWidth: 300 }}
/> />
) : ( ) : (
<h4 <h4
className={styles.canvasTitle} className={styles.canvasHeaderTitle}
style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }} style={{ cursor: onWorkflowRename ? 'pointer' : 'default' }}
onClick={_startNameEdit} onClick={_startNameEdit}
title={onWorkflowRename ? t('Klicken zum Umbenennen') : undefined} title={_titleHint}
> >
{currentWorkflow.label} {currentWorkflow.label}
</h4> </h4>
) )
) : ( ) : (
<h4 className={styles.canvasTitle} style={{ margin: 0, fontStyle: 'italic', opacity: 0.6 }}> <h4 className={`${styles.canvasHeaderTitle} ${styles.canvasHeaderTitleMuted}`}>
{t('Neuer Workflow')} {t('Neuer Workflow')}
</h4> </h4>
)} )}
</div>
{onWorkflowSettings && ( {onWorkflowSettings && (
<button <button
type="button" type="button"
@ -177,37 +209,45 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
<FaCog /> <FaCog />
</button> </button>
)} )}
</div>
{/* Split "Neu" button */} <div className={styles.canvasHeaderActionPanel} role="toolbar" aria-label={t('Workflow-Aktionen')}>
<div ref={newMenuRef} style={{ position: 'relative', display: 'inline-block' }}> <div ref={newMenuRef} className={styles.canvasHeaderNewSplit}>
<div style={{ display: 'flex' }}> <div className={styles.canvasHeaderSplitPair}>
<button type="button" className={styles.retryButton} onClick={onNew} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}> <button
type="button"
className={`${styles.retryButton} ${styles.canvasHeaderNewSplitMain}`}
onClick={onNew}
>
{t('Neu')} {t('Neu')}
</button> </button>
<button <button
type="button" type="button"
className={styles.retryButton} className={`${styles.retryButton} ${styles.canvasHeaderNewSplitMenu}`}
onClick={() => setNewMenuOpen((p) => !p)} onClick={() => setNewMenuOpen((p) => !p)}
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0, paddingLeft: 4, paddingRight: 6, borderLeft: '1px solid rgba(0,0,0,0.15)' }}
title={t('Neu aus Vorlage')} title={t('Neu aus Vorlage')}
aria-haspopup="menu"
aria-expanded={newMenuOpen}
> >
<FaCaretDown style={{ fontSize: '0.7rem' }} /> <FaCaretDown style={{ fontSize: '0.7rem' }} />
</button> </button>
</div> </div>
{newMenuOpen && ( {newMenuOpen && (
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 100, background: 'var(--bg-primary, #fff)', border: '1px solid var(--border-color, #e0e0e0)', borderRadius: 6, boxShadow: '0 4px 12px rgba(0,0,0,0.15)', minWidth: 180, marginTop: 4 }}> <div className={styles.canvasHeaderMenuDropdown} role="menu">
<button <button
type="button" type="button"
className={styles.canvasHeaderMenuItem}
onClick={() => { onNew(); setNewMenuOpen(false); }} onClick={() => { onNew(); setNewMenuOpen(false); }}
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem' }} role="menuitem"
> >
{t('Leerer Workflow')} {t('Leerer Workflow')}
</button> </button>
{onNewFromTemplate && ( {onNewFromTemplate && (
<button <button
type="button" type="button"
className={styles.canvasHeaderMenuItem}
onClick={() => { onNewFromTemplate(); setNewMenuOpen(false); }} onClick={() => { onNewFromTemplate(); setNewMenuOpen(false); }}
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem', borderTop: '1px solid var(--border-color, #e0e0e0)' }} role="menuitem"
> >
{t('Aus Vorlage…')} {t('Aus Vorlage…')}
</button> </button>
@ -220,9 +260,6 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
type="button" type="button"
className={styles.retryButton} className={styles.retryButton}
onClick={onSave} onClick={onSave}
// Phase-4 Schicht-4: Save niemals blockieren — work-in-progress muss
// jederzeit persistierbar sein. Nur während des Save-Requests selbst
// sperren wir den Button, um Doppelklicks zu verhindern.
disabled={saving} disabled={saving}
title={!hasNodes ? t('Workflow ist leer — Speichern legt einen leeren Workflow an.') : undefined} title={!hasNodes ? t('Workflow ist leer — Speichern legt einen leeren Workflow an.') : undefined}
> >
@ -242,26 +279,28 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
</button> </button>
)} )}
{/* Save as template */}
{currentWorkflowId && onSaveAsTemplate && ( {currentWorkflowId && onSaveAsTemplate && (
<div ref={templateMenuRef} style={{ position: 'relative', display: 'inline-block' }}> <div ref={templateMenuRef} className={styles.canvasHeaderNewSplit}>
<button <button
type="button" type="button"
className={styles.retryButton} className={styles.retryButton}
onClick={() => setTemplateMenuOpen((p) => !p)} onClick={() => setTemplateMenuOpen((p) => !p)}
disabled={templateSaving} disabled={templateSaving}
title={t('Als Vorlage speichern')} title={t('Als Vorlage speichern')}
aria-haspopup="menu"
aria-expanded={templateMenuOpen}
> >
{templateSaving ? <FaSpinner className={styles.spinner} /> : <><FaBookmark style={{ marginRight: 4 }} />{t('Als Vorlage')}</>} {templateSaving ? <FaSpinner className={styles.spinner} /> : <><FaBookmark style={{ marginRight: 4 }} />{t('Als Vorlage')}</>}
</button> </button>
{templateMenuOpen && ( {templateMenuOpen && (
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 100, background: 'var(--bg-primary, #fff)', border: '1px solid var(--border-color, #e0e0e0)', borderRadius: 6, boxShadow: '0 4px 12px rgba(0,0,0,0.15)', minWidth: 180, marginTop: 4 }}> <div className={styles.canvasHeaderMenuDropdown} role="menu">
{(['user', 'instance', 'mandate'] as const).map((s) => ( {(['user', 'instance', 'mandate'] as const).map((s) => (
<button <button
key={s} key={s}
type="button" type="button"
className={styles.canvasHeaderMenuItem}
onClick={() => { onSaveAsTemplate(s); setTemplateMenuOpen(false); }} onClick={() => { onSaveAsTemplate(s); setTemplateMenuOpen(false); }}
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem', borderTop: s !== 'user' ? '1px solid var(--border-color, #e0e0e0)' : undefined }} role="menuitem"
> >
{scopeLabels[s]} {scopeLabels[s]}
</button> </button>
@ -270,24 +309,10 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
)} )}
</div> </div>
)} )}
<select
value={currentWorkflowId ?? ''}
onChange={(e) => {
const id = e.target.value ? e.target.value : null;
onWorkflowSelect(id);
}}
style={{ padding: '0.4rem', minWidth: 180 }}
>
<option value="">{t('Workflow laden')}</option>
{workflows.map((w) => (
<option key={w.id} value={w.id}>
{w.label}
</option>
))}
</select>
<button <button
type="button" type="button"
className={styles.retryButton} className={`${styles.retryButton} ${styles.canvasHeaderRunButton}`}
onClick={() => { onClick={() => {
if (executeBlockedReason) { if (executeBlockedReason) {
onExecuteBlockedClick?.(); onExecuteBlockedClick?.();
@ -311,17 +336,17 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
> >
{executing ? ( {executing ? (
<> <>
<FaSpinner className={styles.spinner} style={{ marginRight: '0.5rem', display: 'inline-block' }} /> <FaSpinner className={styles.spinner} style={{ flexShrink: 0 }} />
{t('Ausführen…')} {t('Ausführen…')}
</> </>
) : executeBlockedReason ? ( ) : executeBlockedReason ? (
<> <>
<FaPlay style={{ marginRight: '0.5rem', opacity: 0.5 }} /> <FaPlay style={{ opacity: 0.5, flexShrink: 0 }} />
{t('Pflicht-Felder fehlen')} {t('Pflicht-Felder fehlen')}
</> </>
) : ( ) : (
<> <>
<FaPlay style={{ marginRight: '0.5rem' }} /> <FaPlay style={{ flexShrink: 0 }} />
{t('Ausführen')} {t('Ausführen')}
</> </>
)} )}
@ -332,17 +357,32 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
{t('Workspace')} {t('Workspace')}
</button> </button>
)} )}
{_isSysAdmin && onVerboseSchemaChange && (
<label
className={styles.canvasHeaderSysadmin}
title={t('Sysadmin-Ansicht: zeigt im Node-Panel das statische Typ-Schema (Eingabe/Ausgabe) und Parameter-Typ-Badges.')}
>
<input
type="checkbox"
checked={!!verboseSchema}
onChange={(e) => onVerboseSchemaChange(e.target.checked)}
style={{ margin: 0 }}
/>
{t('Schema-Details')}
</label>
)}
</div>
</div> </div>
{/* Version Selector */}
{currentWorkflowId && versions && versions.length > 0 && ( {currentWorkflowId && versions && versions.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.5rem', flexWrap: 'wrap' }}> <div className={styles.canvasHeaderVersionRow}>
<span style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary, #666)' }}>{t('Version:')}</span> <span className={styles.canvasHeaderVersionLabel}>{t('Version:')}</span>
<select <select
className={styles.canvasHeaderVersionSelect}
value={currentVersionId ?? ''} value={currentVersionId ?? ''}
onChange={(e) => onVersionSelect?.(e.target.value || null)} onChange={(e) => onVersionSelect?.(e.target.value || null)}
style={{ padding: '0.3rem', minWidth: 140, fontSize: '0.85rem' }}
disabled={versionLoading} disabled={versionLoading}
aria-label={t('Version')}
> >
<option value="">{t('Aktuelle')}</option> <option value="">{t('Aktuelle')}</option>
{versions.map((v) => ( {versions.map((v) => (

View file

@ -25,6 +25,9 @@ interface NodeConfigPanelProps {
onNodeUpdate?: (nodeId: string, updates: Partial<Pick<CanvasNode, 'title' | 'comment'>>) => void; onNodeUpdate?: (nodeId: string, updates: Partial<Pick<CanvasNode, 'title' | 'comment'>>) => void;
instanceId?: string; instanceId?: string;
request?: ApiRequestFunction; request?: ApiRequestFunction;
/** When true, render developer-oriented sections (Schema-Typ-Referenz,
* parameter type-badges). Toggle in CanvasHeader, sysadmin-only. */
verboseSchema?: boolean;
} }
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node, export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
@ -35,6 +38,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
onNodeUpdate, onNodeUpdate,
instanceId, instanceId,
request, request,
verboseSchema = false,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [params, setParams] = useState<Record<string, unknown>>({}); const [params, setParams] = useState<Record<string, unknown>>({});
@ -88,11 +92,28 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
}, [nodeType?.parameters]); }, [nodeType?.parameters]);
// Pre-compute which required params are unbound on this node so we can // Pre-compute which required params are unbound on this node so we can
// surface a panel-level summary banner. // surface a panel-level summary banner. The hidden-param safety net lives
// inside `findRequiredErrors` so banner, canvas badges and Run-button stay
// in lockstep.
// Banner labels are kept short (`param.name`); the full description is
// attached as the tooltip below.
const requiredErrors = useMemo(() => { const requiredErrors = useMemo(() => {
if (!node || !nodeType) return []; if (!node || !nodeType) return [];
return findRequiredErrors(node, nodeType, (p) => getLabel(p.description, language) || p.name); return findRequiredErrors(node, nodeType, (p) => p.name);
}, [node, nodeType, language]); }, [node, nodeType]);
// Resolve full descriptions per missing param (for the banner tooltip).
const requiredErrorTooltip = useMemo(() => {
if (!requiredErrors.length || !nodeType) return '';
const byName = new Map((nodeType.parameters ?? []).map((p) => [p.name, p]));
return requiredErrors
.map((e) => {
const p = byName.get(e.paramName);
const desc = p ? (getLabel(p.description, language) || '') : '';
return desc ? `${e.paramName}: ${desc}` : e.paramName;
})
.join('\n');
}, [requiredErrors, nodeType, language]);
if (!node || !nodeType) return null; if (!node || !nodeType) return null;
@ -129,23 +150,23 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
{getLabel(nodeType.description, language)} {getLabel(nodeType.description, language)}
</p> </p>
)} )}
{hasPortInfo && ( {hasPortInfo && verboseSchema && (
<details className={styles.nodeConfigPorts ?? ''} style={{ margin: '0 0 0.75rem', fontSize: '0.7rem' }}> <details className={styles.nodeConfigPorts ?? ''} style={{ margin: '0 0 0.75rem', fontSize: '0.7rem' }}>
<summary <summary
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
color: 'var(--text-secondary, #888)', color: 'var(--text-secondary)',
fontWeight: 500, fontWeight: 500,
padding: '0.15rem 0', padding: '0.15rem 0',
fontStyle: 'italic', fontStyle: 'italic',
}} }}
title={t('Statische Schema-Referenz f\u00fcr diesen Node-Typ \u2014 keine Live-Daten')} title={t('Statische Schema-Referenz f\u00fcr diesen Node-Typ \u2014 keine Live-Daten')}
> >
{t('Schema (Typ-Referenz)')} {t('Schema (Typ-Referenz, Sysadmin-Ansicht)')}
</summary> </summary>
{inputPortEntries.length > 0 && ( {inputPortEntries.length > 0 && (
<div style={{ marginTop: '0.4rem' }}> <div style={{ marginTop: '0.4rem' }}>
<div style={{ color: 'var(--text-secondary, #666)', fontWeight: 600, marginBottom: 2 }}> <div style={{ color: 'var(--text-secondary)', fontWeight: 600, marginBottom: 2 }}>
{'\u2B07'} {t('Eingabe')} {'\u2B07'} {t('Eingabe')}
</div> </div>
{inputPortEntries.map(([idx, def]) => ( {inputPortEntries.map(([idx, def]) => (
@ -162,7 +183,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
)} )}
{outputPortEntries.length > 0 && ( {outputPortEntries.length > 0 && (
<div style={{ marginTop: '0.4rem' }}> <div style={{ marginTop: '0.4rem' }}>
<div style={{ color: 'var(--text-secondary, #666)', fontWeight: 600, marginBottom: 2 }}> <div style={{ color: 'var(--text-secondary)', fontWeight: 600, marginBottom: 2 }}>
{'\u2B06'} {t('Ausgabe')} {'\u2B06'} {t('Ausgabe')}
</div> </div>
{outputPortEntries.map(([idx, def]) => ( {outputPortEntries.map(([idx, def]) => (
@ -189,13 +210,19 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
borderRadius: 4, borderRadius: 4,
fontSize: 12, fontSize: 12,
color: 'var(--danger-color, #dc3545)', color: 'var(--danger-color, #dc3545)',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}} }}
title={requiredErrorTooltip || undefined}
> >
{t('Pflicht-Felder ohne Quelle:')}{' '} {t('Pflicht-Felder ohne Quelle:')}{' '}
<strong>{requiredErrors.map((e) => e.paramLabel).join(', ')}</strong> <strong>{requiredErrors.map((e) => e.paramLabel).join(', ')}</strong>
</div> </div>
)} )}
{parameters.map((param: NodeTypeParameter) => { {parameters.map((param: NodeTypeParameter) => {
// Safety net: hidden params have no UI footprint at all — no row,
// no required-mark, no type-badge. Their value is system-set.
if (param.frontendType === 'hidden') return null;
const useRequiredPicker = _shouldUseRequiredPicker(param); const useRequiredPicker = _shouldUseRequiredPicker(param);
if (useRequiredPicker) { if (useRequiredPicker) {
return ( return (
@ -212,26 +239,40 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
const frontendType = param.frontendType || 'text'; const frontendType = param.frontendType || 'text';
const Renderer = FRONTEND_TYPE_RENDERERS[frontendType] ?? FRONTEND_TYPE_RENDERERS.text; const Renderer = FRONTEND_TYPE_RENDERERS[frontendType] ?? FRONTEND_TYPE_RENDERERS.text;
return ( return (
<div key={param.name} style={{ marginBottom: 4 }}> <div key={param.name} style={{ marginBottom: 4, minWidth: 0, maxWidth: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 2 }}> <div
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
marginBottom: 2,
flexWrap: 'wrap',
minWidth: 0,
}}
>
{param.required && ( {param.required && (
<span <span
title={t('Pflichtfeld')} title={t('Pflichtfeld')}
style={{ color: 'var(--danger-color, #dc3545)', fontWeight: 700 }} style={{ color: 'var(--danger-color, #dc3545)', fontWeight: 700, flexShrink: 0 }}
> >
* *
</span> </span>
)} )}
{param.type && ( {verboseSchema && param.type && (
<span <span
title={t('Parameter-Typ')} title={t('Parameter-Typ')}
style={{ style={{
fontSize: 10, fontSize: 10,
fontWeight: 600, fontWeight: 600,
color: '#555', color: 'var(--text-secondary)',
background: '#eee', background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)',
borderRadius: 4, borderRadius: 4,
padding: '1px 6px', padding: '1px 6px',
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} }}
> >
{param.type} {param.type}
@ -261,6 +302,9 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
function _shouldUseRequiredPicker(param: NodeTypeParameter): boolean { function _shouldUseRequiredPicker(param: NodeTypeParameter): boolean {
if (!param.required) return false; if (!param.required) return false;
if (!param.type) return false; if (!param.type) return false;
// Hidden params never get a picker — they are system-set or rendered to
// nothing on purpose. The render loop above also skips hidden rows entirely.
if (param.frontendType === 'hidden') return false;
// Always defer to specialized FE renderers when explicitly chosen. // Always defer to specialized FE renderers when explicitly chosen.
if (param.frontendType && _LEGACY_RENDERERS_THAT_HANDLE_BINDINGS.has(param.frontendType)) { if (param.frontendType && _LEGACY_RENDERERS_THAT_HANDLE_BINDINGS.has(param.frontendType)) {
return false; return false;
@ -273,6 +317,7 @@ function _shouldUseRequiredPicker(param: NodeTypeParameter): boolean {
const _LEGACY_RENDERERS_THAT_HANDLE_BINDINGS = new Set([ const _LEGACY_RENDERERS_THAT_HANDLE_BINDINGS = new Set([
'userConnection', 'userConnection',
'featureInstance',
'sharepointFolder', 'sharepointFolder',
'sharepointFile', 'sharepointFile',
'clickupList', 'clickupList',
@ -308,7 +353,7 @@ const _PortFieldList: React.FC<_PortFieldListProps> = ({ portIndex, schemaNames,
if (!schemaNames.length) return null; if (!schemaNames.length) return null;
return ( return (
<div style={{ marginLeft: 4, marginBottom: 4 }}> <div style={{ marginLeft: 4, marginBottom: 4 }}>
<div style={{ color: '#888', fontSize: '0.7rem' }}> <div style={{ color: 'var(--text-secondary)', fontSize: '0.7rem' }}>
{`#${portIndex} `}{schemaNames.join(' | ')} {`#${portIndex} `}{schemaNames.join(' | ')}
</div> </div>
{schemaNames.map((name) => { {schemaNames.map((name) => {
@ -316,14 +361,14 @@ const _PortFieldList: React.FC<_PortFieldListProps> = ({ portIndex, schemaNames,
const fields = schema?.fields ?? []; const fields = schema?.fields ?? [];
if (name === 'Transit') { if (name === 'Transit') {
return ( return (
<div key={name} style={{ marginLeft: 8, color: '#999', fontStyle: 'italic', fontSize: '0.7rem' }}> <div key={name} style={{ marginLeft: 8, color: 'var(--text-tertiary)', fontStyle: 'italic', fontSize: '0.7rem' }}>
{'\u00B7 Transit (durchgereichte Daten)'} {'\u00B7 Transit (durchgereichte Daten)'}
</div> </div>
); );
} }
if (!fields.length) { if (!fields.length) {
return ( return (
<div key={name} style={{ marginLeft: 8, color: '#bbb', fontSize: '0.7rem' }}> <div key={name} style={{ marginLeft: 8, color: 'var(--text-tertiary)', fontSize: '0.7rem' }}>
{`\u00B7 ${emptyLabel}`} {`\u00B7 ${emptyLabel}`}
</div> </div>
); );
@ -331,12 +376,12 @@ const _PortFieldList: React.FC<_PortFieldListProps> = ({ portIndex, schemaNames,
return ( return (
<ul key={name} style={{ margin: '2px 0 4px 16px', padding: 0, listStyle: 'none' }}> <ul key={name} style={{ margin: '2px 0 4px 16px', padding: 0, listStyle: 'none' }}>
{fields.map((f) => ( {fields.map((f) => (
<li key={f.name} style={{ fontSize: '0.7rem', lineHeight: 1.4, color: '#555' }}> <li key={f.name} style={{ fontSize: '0.7rem', lineHeight: 1.4, color: 'var(--text-secondary)' }}>
<span style={{ fontFamily: 'monospace', color: '#222' }}>{f.name}</span> <span style={{ fontFamily: 'monospace', color: 'var(--text-primary)' }}>{f.name}</span>
<span style={{ color: '#999' }}>{`: ${f.type}`}</span> <span style={{ color: 'var(--text-tertiary)' }}>{`: ${f.type}`}</span>
{!f.required && <span style={{ color: '#bbb' }}>{' (optional)'}</span>} {!f.required && <span style={{ color: 'var(--text-tertiary)' }}>{' (optional)'}</span>}
{f.description && ( {f.description && (
<div style={{ color: '#888', marginLeft: 4 }}> <div style={{ color: 'var(--text-secondary)', marginLeft: 4 }}>
{getLabel(f.description, language)} {getLabel(f.description, language)}
</div> </div>
)} )}

View file

@ -0,0 +1,158 @@
/**
* FeatureInstancePicker renderer for frontendType="featureInstance".
*
* Modeled on ConnectionPicker. Loads mandate-scoped FeatureInstances filtered
* by `frontendOptions.featureCode` (e.g. "trustee", "redmine") via
* GET /api/workflows/{instanceId}/options/feature.instance?featureCode=<code>
*
* Behavior matches the rest of the editor:
* - 0 results -> hint to create a feature instance for this mandate
* - 1 result -> auto-pick (no manual click required)
* - N results -> <select>
*
* The bound value is a plain `<id>` string so backend adapters can keep
* using `featureInstanceId` lookups unchanged. Type stays
* `FeatureInstanceRef[<code>]` on the parameter so DataPicker / RequiredAttributePicker
* filter correctly.
*/
import React from 'react';
import { useLanguage } from '../../../../providers/language/LanguageContext';
import type { FieldRendererProps } from './index';
type FeatureInstanceOption = { id: string; label: string };
export const FeatureInstancePicker: React.FC<FieldRendererProps> = ({
param,
value,
onChange,
instanceId,
request,
}) => {
const { t } = useLanguage();
const featureCode =
(param.frontendOptions?.featureCode as string | undefined) || undefined;
const [instances, setInstances] = React.useState<FeatureInstanceOption[]>([]);
const [loading, setLoading] = React.useState(false);
const [loadError, setLoadError] = React.useState<string | null>(null);
const autoSingleRef = React.useRef(false);
React.useEffect(() => {
if (!instanceId || !request || !featureCode) return;
setLoading(true);
setLoadError(null);
request({
url: `/api/workflows/${instanceId}/options/feature.instance?featureCode=${encodeURIComponent(featureCode)}`,
method: 'get',
})
.then((res: unknown) => {
const data = res as { options?: Array<{ value: string; label: string }> };
setInstances((data?.options || []).map((o) => ({ id: o.value, label: o.label })));
})
.catch((err: unknown) => {
console.error('FeatureInstancePicker: failed to load instances', err);
setInstances([]);
setLoadError(err instanceof Error ? err.message : String(err));
})
.finally(() => setLoading(false));
}, [instanceId, request, featureCode]);
React.useEffect(() => {
if (instances.length !== 1 || autoSingleRef.current) return;
if (value !== '' && value !== undefined && value !== null) return;
autoSingleRef.current = true;
onChange(instances[0].id);
}, [instances, value, onChange]);
const strVal = typeof value === 'string' ? value : '';
const codeLabel = featureCode ?? t('Feature');
return (
<div style={{ marginBottom: 8, minWidth: 0, maxWidth: '100%' }}>
<label
style={{
display: 'block',
fontSize: 12,
marginBottom: 2,
color: 'var(--text-primary)',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
>
{param.description || param.name}
</label>
{loading && (
<div style={{ fontSize: 11, color: 'var(--text-secondary)' }}>{t('Lade…')}</div>
)}
{!loading && instances.length === 0 && !loadError && (
<div
style={{
fontSize: 11,
color: 'var(--text-secondary)',
marginBottom: 4,
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
>
{t('Keine {code}-Instanz im aktiven Mandanten — bitte in der Admin-Konsole anlegen.', { code: codeLabel })}
</div>
)}
{!loading && instances.length === 1 && (
<div
style={{
fontSize: 12,
marginBottom: 4,
color: 'var(--text-primary)',
background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)',
borderRadius: 4,
padding: '4px 8px',
maxWidth: '100%',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
lineHeight: 1.4,
}}
title={`${t('Einziger {code}-Mandant — automatisch gewählt.', { code: codeLabel })} — ${instances[0].label}`}
>
{instances[0].label}
</div>
)}
{!loading && instances.length > 1 && (
<select
value={strVal}
onChange={(e) => onChange(e.target.value)}
style={{
width: '100%',
maxWidth: '100%',
boxSizing: 'border-box',
padding: '4px 8px',
borderRadius: 4,
border: '1px solid var(--border-color)',
background: 'var(--bg-primary)',
color: 'var(--text-primary)',
}}
>
<option value="">{t('{code}-Mandant wählen', { code: codeLabel })}</option>
{instances.map((c) => (
<option key={c.id} value={c.id}>{c.label}</option>
))}
</select>
)}
{loadError && (
<div
style={{
fontSize: 11,
color: 'var(--danger-color, #c00)',
marginTop: 2,
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
>
{t('Mandanten-Liste konnte nicht geladen werden')}
</div>
)}
</div>
);
};
export default FeatureInstancePicker;

View file

@ -31,6 +31,7 @@ import { toApiGraph } from '../shared/graphUtils';
import { postUpstreamPaths } from '../../../../api/workflowApi'; import { postUpstreamPaths } from '../../../../api/workflowApi';
import type { CanvasNode } from '../../editor/FlowCanvas'; import type { CanvasNode } from '../../editor/FlowCanvas';
import { DataRefRenderer } from './DataRefRenderer'; import { DataRefRenderer } from './DataRefRenderer';
import { FeatureInstancePicker } from './FeatureInstancePicker';
const TextInput: React.FC<FieldRendererProps> = ({ param, value, onChange }) => ( const TextInput: React.FC<FieldRendererProps> = ({ param, value, onChange }) => (
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
@ -755,6 +756,7 @@ export const FRONTEND_TYPE_RENDERERS: Record<string, FieldRendererComponent> = {
hidden: HiddenInput, hidden: HiddenInput,
dataRef: DataRefRenderer, dataRef: DataRefRenderer,
userConnection: ConnectionPicker, userConnection: ConnectionPicker,
featureInstance: FeatureInstancePicker,
sharepointFolder: SharepointPathPicker, sharepointFolder: SharepointPathPicker,
sharepointFile: SharepointPathPicker, sharepointFile: SharepointPathPicker,
clickupList: FolderPicker, clickupList: FolderPicker,

View file

@ -6,6 +6,7 @@
*/ */
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef'; import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef';
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
import type { GraphDefinedSchemaRef, NodeType, PortSchema } from '../../../../api/workflowApi'; import type { GraphDefinedSchemaRef, NodeType, PortSchema } from '../../../../api/workflowApi';
@ -254,33 +255,35 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
onClose(); onClose();
}; };
return ( const _dialog = (
<div className={styles.dataPickerOverlay} onClick={onClose}> <div
<div className={styles.dataPickerModal} onClick={(e) => e.stopPropagation()}> className={styles.dataPickerOverlay}
onClick={onClose}
onKeyDown={(e) => e.key === 'Escape' && onClose()}
role="presentation"
>
<div
className={styles.dataPickerModal}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="automation2DataPickerTitle"
>
<div className={styles.dataPickerHeader}> <div className={styles.dataPickerHeader}>
<h4 className={styles.dataPickerTitle}> <h4 className={styles.dataPickerTitle} id="automation2DataPickerTitle">
{t('Datenquelle wählen')} {t('Datenquelle wählen')}
{expectedParamType && ( {expectedParamType && (
<span <span
style={{ className={styles.dataPickerTypeBadge}
marginLeft: 8,
fontSize: 11,
fontFamily: 'monospace',
color: '#555',
background: '#eee',
borderRadius: 4,
padding: '1px 6px',
fontWeight: 400,
}}
title={t('Erwarteter Typ')} title={t('Erwarteter Typ')}
> >
{expectedParamType} {expectedParamType}
</span> </span>
)} )}
</h4> </h4>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div className={styles.dataPickerHeaderControls}>
{expectedParamType && ( {expectedParamType && (
<label style={{ fontSize: 11, color: '#666', display: 'flex', alignItems: 'center', gap: 4 }}> <label className={styles.dataPickerStrictLabel}>
<input <input
type="checkbox" type="checkbox"
checked={strictFilter} checked={strictFilter}
@ -315,7 +318,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
]; ];
return ( return (
<div key={loopId} style={{ marginBottom: 6 }}> <div key={loopId} style={{ marginBottom: 6 }}>
<div style={{ fontSize: 11, color: '#666', marginBottom: 2 }}>{loopLabel}</div> <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginBottom: 2 }}>{loopLabel}</div>
{loopPaths.map((p, i) => { {loopPaths.map((p, i) => {
const compat = expectedParamType && p.type const compat = expectedParamType && p.type
? isCompatible(p.type, expectedParamType) ? isCompatible(p.type, expectedParamType)
@ -330,7 +333,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
> >
{p.label} {p.label}
{p.type && ( {p.type && (
<span style={{ color: '#888', fontSize: 10, marginLeft: 4 }}> <span className={styles.dataPickerLeafType}>
({p.type}) ({p.type})
</span> </span>
)} )}
@ -364,7 +367,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
onClick={() => handlePickSystemVar(key)} onClick={() => handlePickSystemVar(key)}
title={info.description} title={info.description}
> >
{key} <span style={{ color: '#888', fontSize: 10 }}>({info.type})</span> {key} <span className={styles.dataPickerLeafType}>({info.type})</span>
</button> </button>
))} ))}
</div> </div>
@ -418,7 +421,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
<span className={styles.dataPickerExpandIcon}>{isExpanded ? '▼' : '▶'}</span> <span className={styles.dataPickerExpandIcon}>{isExpanded ? '▼' : '▶'}</span>
<span className={styles.dataPickerNodeLabel}>{label}</span> <span className={styles.dataPickerNodeLabel}>{label}</span>
{resolvedSchema && ( {resolvedSchema && (
<span style={{ color: '#888', fontSize: 10, marginLeft: 4 }}> <span className={styles.dataPickerNodeSchemaHint}>
({resolvedSchema.name}) ({resolvedSchema.name})
</span> </span>
)} )}
@ -426,7 +429,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
{isExpanded && ( {isExpanded && (
<div className={styles.dataPickerTree}> <div className={styles.dataPickerTree}>
{paths.length === 0 && ( {paths.length === 0 && (
<div style={{ fontSize: 11, color: '#999', padding: '4px 8px' }}> <div style={{ fontSize: 11, color: 'var(--text-secondary)', padding: '4px 8px' }}>
{t('(keine kompatiblen Felder — Filter „Nur kompatible“ deaktivieren)')} {t('(keine kompatiblen Felder — Filter „Nur kompatible“ deaktivieren)')}
</div> </div>
)} )}
@ -446,7 +449,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
> >
{p.label} {p.label}
{p.type && ( {p.type && (
<span style={{ color: '#888', fontSize: 10, marginLeft: 4 }}> <span className={styles.dataPickerLeafType}>
({p.type}) ({p.type})
</span> </span>
)} )}
@ -454,15 +457,8 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
{p.iterable && ( {p.iterable && (
<button <button
type="button" type="button"
className={styles.dataPickerLeaf} className={`${styles.dataPickerLeaf} ${styles.dataPickerIterateBtn}`}
onClick={() => handlePickIterate(nodeId, p.path, expectedParamType)} onClick={() => handlePickIterate(nodeId, p.path, expectedParamType)}
style={{
fontSize: 10,
padding: '2px 6px',
background: 'rgba(0,123,255,0.10)',
color: 'var(--primary-color, #007bff)',
whiteSpace: 'nowrap',
}}
title={t('Pro Element der Liste iterieren (Loop)')} title={t('Pro Element der Liste iterieren (Loop)')}
> >
{t('iterieren')} {t('iterieren')}
@ -481,4 +477,6 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
</div> </div>
</div> </div>
); );
return createPortal(_dialog, document.body);
}; };

View file

@ -91,9 +91,30 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
}; };
return ( return (
<div className={styles.requiredAttributePicker} style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}> className={styles.requiredAttributePicker}
<label style={{ fontSize: 12, fontWeight: 600 }}> style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
minWidth: 0,
maxWidth: '100%',
}}
>
{/* Header: label always takes the full row (flex-basis 100 %), badge
wraps below prevents long type names like List[ActionDocument]
from escaping the panel frame on the right. */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, flexWrap: 'wrap' }}>
<label
style={{
fontSize: 12,
fontWeight: 600,
flex: '1 1 100%',
minWidth: 0,
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
>
{label} {label}
<span style={{ color: 'var(--danger-color, #dc3545)', marginLeft: 2 }}>*</span> <span style={{ color: 'var(--danger-color, #dc3545)', marginLeft: 2 }}>*</span>
</label> </label>
@ -103,10 +124,15 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
style={{ style={{
fontSize: 10, fontSize: 10,
fontFamily: 'monospace', fontFamily: 'monospace',
color: '#555', color: 'var(--text-secondary, #555)',
background: '#eee', background: 'var(--bg-secondary, #eee)',
border: '1px solid var(--border-color, #e0e0e0)',
borderRadius: 4, borderRadius: 4,
padding: '1px 6px', padding: '1px 6px',
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} }}
> >
{expectedType} {expectedType}
@ -115,8 +141,9 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
</div> </div>
{isBoundRef ? ( {isBoundRef ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', minWidth: 0 }}>
<span <span
title={typeof boundLabel === 'string' ? boundLabel : undefined}
style={{ style={{
padding: '2px 8px', padding: '2px 8px',
borderRadius: 12, borderRadius: 12,
@ -124,6 +151,10 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
color: 'var(--success-color, #28a745)', color: 'var(--success-color, #28a745)',
fontSize: 12, fontSize: 12,
fontWeight: 500, fontWeight: 500,
maxWidth: '100%',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
lineHeight: 1.4,
}} }}
> >
{boundLabel} {boundLabel}
@ -132,7 +163,7 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
type="button" type="button"
className={styles.retryButton} className={styles.retryButton}
onClick={() => setPickerOpen(true)} onClick={() => setPickerOpen(true)}
style={{ fontSize: 11, padding: '2px 8px' }} style={{ fontSize: 11, padding: '2px 8px', flexShrink: 0 }}
> >
{t('Andere wählen…')} {t('Andere wählen…')}
</button> </button>
@ -140,7 +171,7 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
type="button" type="button"
className={styles.retryButton} className={styles.retryButton}
onClick={() => onChange(null)} onClick={() => onChange(null)}
style={{ fontSize: 11, padding: '2px 8px' }} style={{ fontSize: 11, padding: '2px 8px', flexShrink: 0 }}
title={t('Bindung entfernen')} title={t('Bindung entfernen')}
> >
× ×
@ -150,29 +181,41 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'flex-start',
gap: 6, gap: 6,
padding: '4px 8px', padding: '4px 8px',
background: 'rgba(220,53,69,0.12)', background: 'rgba(220,53,69,0.12)',
color: 'var(--danger-color, #dc3545)', color: 'var(--danger-color, #dc3545)',
borderRadius: 6, borderRadius: 6,
fontSize: 12, fontSize: 12,
minWidth: 0,
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}} }}
> >
<span aria-hidden="true"></span> <span aria-hidden="true" style={{ flexShrink: 0 }}></span>
<span> <span style={{ minWidth: 0 }}>
{t('Keine typkompatible Quelle vorhanden — füge zuerst einen Knoten ein, der ')} {t('Keine typkompatible Quelle vorhanden — füge zuerst einen Knoten ein, der ')}
<code style={{ fontFamily: 'monospace' }}>{expectedType ?? '?'}</code> <code style={{ fontFamily: 'monospace', overflowWrap: 'anywhere' }}>{expectedType ?? '?'}</code>
{t(' liefert.')} {t(' liefert.')}
</span> </span>
</div> </div>
) : single ? ( ) : single ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', minWidth: 0 }}>
<button <button
type="button" type="button"
className={styles.retryButton} className={styles.retryButton}
onClick={handleAutoBind} onClick={handleAutoBind}
style={{ fontSize: 11, padding: '3px 10px' }} style={{
fontSize: 11,
padding: '3px 10px',
maxWidth: '100%',
whiteSpace: 'normal',
textAlign: 'left',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
lineHeight: 1.4,
}}
title={t('Einzige passende Quelle übernehmen')} title={t('Einzige passende Quelle übernehmen')}
> >
{t('Vorschlag übernehmen:')}{' '} {t('Vorschlag übernehmen:')}{' '}
@ -186,25 +229,36 @@ export const RequiredAttributePicker: React.FC<RequiredAttributePickerProps> = (
type="button" type="button"
className={styles.retryButton} className={styles.retryButton}
onClick={() => setPickerOpen(true)} onClick={() => setPickerOpen(true)}
style={{ fontSize: 11, padding: '3px 10px' }} style={{ fontSize: 11, padding: '3px 10px', flexShrink: 0 }}
> >
{t('Andere…')} {t('Andere…')}
</button> </button>
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', minWidth: 0 }}>
<button <button
type="button" type="button"
className={styles.retryButton} className={styles.retryButton}
onClick={() => setPickerOpen(true)} onClick={() => setPickerOpen(true)}
style={{ fontSize: 11, padding: '3px 10px' }} style={{ fontSize: 11, padding: '3px 10px', maxWidth: '100%' }}
> >
{t('Quelle wählen…')} <span style={{ opacity: 0.6 }}>({candidateCount})</span> {t('Quelle wählen…')} <span style={{ opacity: 0.6 }}>({candidateCount})</span>
</button> </button>
</div> </div>
)} )}
{description && <div style={{ fontSize: 11, color: '#888' }}>{description}</div>} {description && (
<div
style={{
fontSize: 11,
color: 'var(--text-tertiary, #888)',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
>
{description}
</div>
)}
{pickerOpen && ( {pickerOpen && (
<DataPicker <DataPicker

View file

@ -174,6 +174,20 @@ describe('findRequiredErrors', () => {
const node = _makeNode('n1', 'ghost.node'); const node = _makeNode('n1', 'ghost.node');
expect(findRequiredErrors(node, undefined)).toEqual([]); expect(findRequiredErrors(node, undefined)).toEqual([]);
}); });
it('skips required params with frontendType="hidden" (UI safety net)', () => {
// Hidden params have no UI surface, so reporting them as
// "Pflichtfeld ohne Quelle" would create a phantom error the user can
// not resolve. They are auto-set by adapters / system defaults.
const node = _makeNode('n1', 'trustee.extractFromFiles', {});
const nodeType = _makeNodeType('trustee.extractFromFiles', 'AiResult', [
{ name: 'prompt', type: 'str', required: true },
{ name: 'systemContext', type: 'str', required: true, frontendType: 'hidden' },
]);
const errs = findRequiredErrors(node, nodeType);
expect(errs).toHaveLength(1);
expect(errs[0]!.paramName).toBe('prompt');
});
}); });
describe('findGraphErrors', () => { describe('findGraphErrors', () => {

View file

@ -64,7 +64,14 @@ export interface RequiredParamError {
paramType?: string; paramType?: string;
} }
/** Walk a node's parameter spec + values and flag every required-but-unbound. */ /** Walk a node's parameter spec + values and flag every required-but-unbound.
*
* Safety net: params with `frontendType: 'hidden'` are excluded they have
* no UI surface (the panel skips them entirely), so reporting them as
* "Pflichtfeld ohne Quelle" would create a phantom error the user cannot
* resolve. Hidden-required params should be auto-set by the adapter or
* caught in tests, never surfaced to end users.
*/
export function findRequiredErrors( export function findRequiredErrors(
node: CanvasNode, node: CanvasNode,
nodeType: NodeType | undefined, nodeType: NodeType | undefined,
@ -75,6 +82,7 @@ export function findRequiredErrors(
const values = node.parameters ?? {}; const values = node.parameters ?? {};
for (const param of nodeType.parameters ?? []) { for (const param of nodeType.parameters ?? []) {
if (!param.required) continue; if (!param.required) continue;
if (param.frontendType === 'hidden') continue;
if (isParamBound(values[param.name])) continue; if (isParamBound(values[param.name])) continue;
errors.push({ paramName: param.name, paramLabel: resolveLabel(param), paramType: param.type }); errors.push({ paramName: param.name, paramLabel: resolveLabel(param), paramType: param.type });
} }

View file

@ -338,12 +338,6 @@
overflow: visible; overflow: visible;
} }
.fkLoading {
color: var(--color-text);
opacity: 0.6;
font-style: italic;
}
/* Rows */ /* Rows */
.tr { .tr {
transition: background-color 0.12s ease; transition: background-color 0.12s ease;

View file

@ -76,8 +76,11 @@ import type { AttributeType } from '../../../utils/attributeTypeMapper';
import { FaFilter } from 'react-icons/fa'; import { FaFilter } from 'react-icons/fa';
import api from '../../../api'; import api from '../../../api';
// FK Cache type: maps fkSource -> { id -> displayLabel } /** A filter value can be a plain string, null (for empty/missing), or a
type FkCacheType = Record<string, Record<string, string>>; * {value, label} object returned by FK-aware filter-values endpoints. */
type FilterValue = string | null | { value: string | null; label: string };
const _EMPTY_FILTER_SENTINEL = '__EMPTY__';
/** /**
* Stringify any cell value for display. * Stringify any cell value for display.
@ -111,8 +114,10 @@ export interface ColumnConfig {
filterOptions?: string[]; // For enum/select filters filterOptions?: string[]; // For enum/select filters
filterLabelResolver?: (value: string) => string; // Map filter value to display label in dropdown filterLabelResolver?: (value: string) => string; // Map filter value to display label in dropdown
cellClassName?: (value: any, row: any) => string; // For custom cell styling cellClassName?: (value: any, row: any) => string; // For custom cell styling
fkSource?: string; // API endpoint for FK resolution (e.g., "/api/users/")
fkDisplayField?: string; // Which field of FK target to display (e.g., "username", "name", "roleLabel") /** Backend-enriched label column, e.g. `mandateId` → `mandateIdLabel` (set by `attributeUtils` / API). */
displayField?: string;
// Backend-provided render hints (gateway/.../attributeUtils.py). // Backend-provided render hints (gateway/.../attributeUtils.py).
// Excel-style format string applied by ``applyFrontendFormat`` to numeric/int // Excel-style format string applied by ``applyFrontendFormat`` to numeric/int
// values, e.g. "R:#'###.00", "M:b" (bytes), "L:0.000". Empty = default rendering. // values, e.g. "R:#'###.00", "M:b" (bytes), "L:0.000". Empty = default rendering.
@ -211,9 +216,29 @@ export interface FormGeneratorTableProps<T = any> {
const _FILTER_PAGE_SIZE = 100; const _FILTER_PAGE_SIZE = 100;
/** Normalize a FilterValue to {value, label}. */
function _normalizeFilterValue(
fv: FilterValue,
resolveLabel?: (value: string) => string,
): { value: string | null; label: string } {
if (fv === null) {
return { value: null, label: '(Leer)' };
}
if (typeof fv === 'object' && 'value' in fv && 'label' in fv) {
return fv as { value: string | null; label: string };
}
const str = String(fv);
return { value: str, label: resolveLabel ? resolveLabel(str) : str };
}
/** /**
* Renders a scrollable list of filter values with IntersectionObserver-based lazy loading. * Renders a scrollable list of filter values with IntersectionObserver-based lazy loading.
* Shows _FILTER_PAGE_SIZE items initially, loads more as the user scrolls. * Shows _FILTER_PAGE_SIZE items initially, loads more as the user scrolls.
*
* Supports three value formats:
* - `string` plain value (legacy)
* - `null` empty/NULL sentinel (rendered as "(Leer)")
* - `{value, label}` backend-resolved FK with display label
*/ */
function FilterValuesList({ function FilterValuesList({
columnKey, columnKey,
@ -223,7 +248,7 @@ function FilterValuesList({
resolveLabel, resolveLabel,
}: { }: {
columnKey: string; columnKey: string;
allValues: string[]; allValues: FilterValue[];
activeFilter: any; activeFilter: any;
onSelect: (value: string) => void; onSelect: (value: string) => void;
resolveLabel?: (value: string) => string; resolveLabel?: (value: string) => string;
@ -242,14 +267,19 @@ function FilterValuesList({
searchInputRef.current?.focus(); searchInputRef.current?.focus();
}, [columnKey]); }, [columnKey]);
// Normalize all values to {value, label} pairs, filtering out nulls
// (null entries are handled separately as the "(Leer)" option)
const normalizedValues = useMemo(() => {
return allValues
.filter(fv => fv !== null)
.map(fv => _normalizeFilterValue(fv, resolveLabel));
}, [allValues, resolveLabel]);
const filteredValues = useMemo(() => { const filteredValues = useMemo(() => {
if (!searchTerm.trim()) return allValues; if (!searchTerm.trim()) return normalizedValues;
const term = searchTerm.toLowerCase(); const term = searchTerm.toLowerCase();
return allValues.filter(value => { return normalizedValues.filter(nv => nv.label.toLowerCase().includes(term));
const label = resolveLabel ? resolveLabel(value) : value; }, [normalizedValues, searchTerm]);
return label.toLowerCase().includes(term);
});
}, [allValues, searchTerm, resolveLabel]);
useEffect(() => { useEffect(() => {
const sentinel = sentinelRef.current; const sentinel = sentinelRef.current;
@ -293,16 +323,16 @@ function FilterValuesList({
/> />
</div> </div>
)} )}
{visibleValues.map(value => { {visibleValues.map(nv => {
const label = resolveLabel ? resolveLabel(value) : value; const selectValue = nv.value === null ? _EMPTY_FILTER_SENTINEL : nv.value;
return ( return (
<div <div
key={value} key={nv.value ?? '__null__'}
className={`${styles.filterOption} ${activeFilter === value ? styles.filterOptionSelected : ''}`} className={`${styles.filterOption} ${activeFilter === nv.value ? styles.filterOptionSelected : ''}`}
onClick={() => onSelect(value)} onClick={() => onSelect(selectValue)}
title={label} title={nv.label}
> >
{label.length > 30 ? label.substring(0, 30) + '...' : label} {nv.label.length > 30 ? nv.label.substring(0, 30) + '...' : nv.label}
</div> </div>
); );
})} })}
@ -519,11 +549,6 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => new Set()); const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => new Set());
const [groupsInitialized, setGroupsInitialized] = useState(false); const [groupsInitialized, setGroupsInitialized] = useState(false);
// FK Resolution: Cache for resolved FK values (fkSource -> { id -> displayLabel })
const [fkCache, setFkCache] = useState<FkCacheType>({});
const [fkLoading, setFkLoading] = useState<Record<string, boolean>>({});
const fkLoadedSourcesRef = useRef<Set<string>>(new Set());
// Generate a storage key based on column names for localStorage persistence // Generate a storage key based on column names for localStorage persistence
const storageKey = useMemo(() => { const storageKey = useMemo(() => {
if (detectedColumns.length === 0) return null; if (detectedColumns.length === 0) return null;
@ -586,7 +611,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
// Note: Date/timestamp filters are disabled in column config, so they won't appear here // Note: Date/timestamp filters are disabled in column config, so they won't appear here
const activeFilters: Record<string, any> = {}; const activeFilters: Record<string, any> = {};
Object.entries(filters).forEach(([key, value]) => { Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== '') { if (value !== undefined) {
activeFilters[key] = value; activeFilters[key] = value;
} }
}); });
@ -739,133 +764,6 @@ export function FormGeneratorTable<T extends Record<string, any>>({
} }
}).current; }).current;
const convertToDisplayString = useCallback((fieldValue: any, _language: string): string => {
if (fieldValue === null || fieldValue === undefined) {
return '-';
}
// Boolean → language-neutral symbols (✓/✗)
if (typeof fieldValue === 'boolean') {
return fieldValue ? '✓' : '✗';
}
// Number → String
if (typeof fieldValue === 'number') {
return String(fieldValue);
}
// String → direct
if (typeof fieldValue === 'string') {
return fieldValue;
}
if (typeof fieldValue === 'object' && fieldValue !== null) {
return _objectToDisplayString(fieldValue as Record<string, unknown>);
}
// Fallback
return String(fieldValue);
}, []);
// FK Resolution: Load FK data in bulk for columns with fkSource
useEffect(() => {
if (data.length === 0 || detectedColumns.length === 0) return;
// Find columns with fkSource that haven't been loaded yet
const fkColumns = detectedColumns.filter(col =>
col.fkSource && !fkLoadedSourcesRef.current.has(col.fkSource)
);
if (fkColumns.length === 0) return;
// For each FK column, collect unique IDs from data and fetch them
const loadFkData = async () => {
for (const column of fkColumns) {
const fkSource = column.fkSource!;
const displayField = column.fkDisplayField; // Explicit field from Pydantic model
// Skip if already loading
if (fkLoading[fkSource]) continue;
// Collect unique IDs from data for this column
const uniqueIds = new Set<string>();
data.forEach(row => {
const value = row[column.key];
if (value && typeof value === 'string' && value.length > 0) {
uniqueIds.add(value);
}
});
if (uniqueIds.size === 0) {
fkLoadedSourcesRef.current.add(fkSource);
continue;
}
// Mark as loading
setFkLoading(prev => ({ ...prev, [fkSource]: true }));
try {
// Fetch all items from the FK source endpoint
const response = await api.get(fkSource);
// Build cache: id -> display label
const cacheForSource: Record<string, string> = {};
const items = Array.isArray(response.data) ? response.data : response.data?.items || [];
items.forEach((item: any) => {
if (!item || !item.id) return;
let displayLabel = item.id; // Fallback to ID
// Use the EXPLICIT display field from Pydantic model (fkDisplayField)
if (displayField && item[displayField] != null && item[displayField] !== '') {
displayLabel = convertToDisplayString(item[displayField], currentLanguage);
} else {
// Fallback: if no displayField specified, try common fields
// This should rarely happen if models are properly configured
const fallbackFields = ['name', 'label', 'username', 'roleLabel', 'title'];
for (const field of fallbackFields) {
if (item[field] !== undefined) {
displayLabel = convertToDisplayString(item[field], currentLanguage);
break;
}
}
}
cacheForSource[item.id] = displayLabel;
});
// Update cache
setFkCache(prev => ({
...prev,
[fkSource]: { ...(prev[fkSource] || {}), ...cacheForSource }
}));
// Mark as loaded
fkLoadedSourcesRef.current.add(fkSource);
} catch (error) {
console.error(`Failed to load FK data from ${fkSource}:`, error);
// Mark as loaded to prevent infinite retries
fkLoadedSourcesRef.current.add(fkSource);
} finally {
setFkLoading(prev => ({ ...prev, [fkSource]: false }));
}
}
};
loadFkData();
}, [data, detectedColumns, currentLanguage, fkLoading, convertToDisplayString]);
// Helper function to resolve FK value to display label
const resolveFkValue = useCallback((value: string, fkSource: string): string => {
const sourceCache = fkCache[fkSource];
if (sourceCache && sourceCache[value]) {
return sourceCache[value];
}
// Return truncated ID while loading or if not found
return value.length > 8 ? `${value.substring(0, 8)}...` : value;
}, [fkCache]);
// Data is already filtered, sorted, and paginated by the backend. // Data is already filtered, sorted, and paginated by the backend.
// Client-side only filters out rows that were just optimistically deleted // Client-side only filters out rows that were just optimistically deleted
// so the UI updates instantly before the server's next refetch response. // so the UI updates instantly before the server's next refetch response.
@ -986,8 +884,10 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const handleFilter = (key: string, value: any, keepOpen = false) => { const handleFilter = (key: string, value: any, keepOpen = false) => {
setFilters(prev => { setFilters(prev => {
const newFilters = { ...prev }; const newFilters = { ...prev };
if (value === undefined || value === '' || value === null) { if (value === undefined) {
delete newFilters[key]; delete newFilters[key];
} else if (value === _EMPTY_FILTER_SENTINEL) {
newFilters[key] = null;
} else { } else {
newFilters[key] = value; newFilters[key] = value;
} }
@ -1024,7 +924,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
}, [filters]); }, [filters]);
// Track which filter columns show all values (expanded beyond initial 100) // Track which filter columns show all values (expanded beyond initial 100)
const [asyncFilterValues, setAsyncFilterValues] = useState<Record<string, string[]>>({}); const [asyncFilterValues, setAsyncFilterValues] = useState<Record<string, FilterValue[]>>({});
const [filterValuesLoading, setFilterValuesLoading] = useState<Record<string, boolean>>({}); const [filterValuesLoading, setFilterValuesLoading] = useState<Record<string, boolean>>({});
// Invalidate cached filter values when filters change (cross-filtering) // Invalidate cached filter values when filters change (cross-filtering)
@ -1045,9 +945,8 @@ export function FormGeneratorTable<T extends Record<string, any>>({
// Skip if column has static filterOptions (enum) those are used directly // Skip if column has static filterOptions (enum) those are used directly
if (column?.filterOptions && column.filterOptions.length > 0) return; if (column?.filterOptions && column.filterOptions.length > 0) return;
// FK columns with backend pagination: still fetch from backend (data is only one page) // displayField + local full dataset: filter values are derived from `data` (see getUniqueValuesForColumn)
// FK columns without backend pagination: skip (data is the full dataset, extracted below) if (column?.displayField && !supportsBackendPagination) return;
if (column?.fkSource && !supportsBackendPagination) return;
// Skip if already loaded or currently loading // Skip if already loaded or currently loading
if (asyncFilterValues[openFilterColumn] || filterValuesLoading[openFilterColumn]) return; if (asyncFilterValues[openFilterColumn] || filterValuesLoading[openFilterColumn]) return;
@ -1055,7 +954,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const _fetchValues = async (columnKey: string) => { const _fetchValues = async (columnKey: string) => {
setFilterValuesLoading(prev => ({ ...prev, [columnKey]: true })); setFilterValuesLoading(prev => ({ ...prev, [columnKey]: true }));
try { try {
let values: string[]; let values: FilterValue[];
if (hookData?.fetchFilterValues && typeof hookData.fetchFilterValues === 'function') { if (hookData?.fetchFilterValues && typeof hookData.fetchFilterValues === 'function') {
const crossFilters: Record<string, any> = {}; const crossFilters: Record<string, any> = {};
Object.entries(filters).forEach(([k, v]) => { Object.entries(filters).forEach(([k, v]) => {
@ -1089,27 +988,28 @@ export function FormGeneratorTable<T extends Record<string, any>>({
// 3) data — ONLY when no backend pagination (data = full dataset) // 3) data — ONLY when no backend pagination (data = full dataset)
// With backend pagination, data is a single page, so extracting filter // With backend pagination, data is a single page, so extracting filter
// values from it would be incomplete and misleading. // values from it would be incomplete and misleading.
const getUniqueValuesForColumn = useCallback((columnKey: string): string[] => { const getUniqueValuesForColumn = useCallback((columnKey: string): FilterValue[] => {
const column = detectedColumns.find(c => c.key === columnKey); const column = detectedColumns.find(c => c.key === columnKey);
if (column?.filterOptions && column.filterOptions.length > 0) { if (column?.filterOptions && column.filterOptions.length > 0) {
return column.filterOptions; return column.filterOptions;
} }
// FK columns without backend pagination: extract from full local data // displayField + local full dataset: { value, label } from enriched rows
if (column?.fkSource && !supportsBackendPagination) { if (column?.displayField && !supportsBackendPagination) {
const seen = new Set<string>(); const showKey = column.displayField;
const byVal = new Map<string, string>();
data.forEach(row => { data.forEach(row => {
const val = row[columnKey]; const val = row[columnKey];
if (val && typeof val === 'string' && val.trim()) { if (val == null || val === '') return;
seen.add(val); const raw = String(val);
} const d = row[showKey];
}); const label = d != null && d !== '' ? String(d) : `NA(${raw})`;
return Array.from(seen).sort((a, b) => { if (!byVal.has(raw)) byVal.set(raw, label);
const labelA = fkCache[column.fkSource!]?.[a] || a;
const labelB = fkCache[column.fkSource!]?.[b] || b;
return labelA.localeCompare(labelB);
}); });
return Array.from(byVal.entries())
.sort((a, b) => a[1].localeCompare(b[1]))
.map(([value, label]) => ({ value, label }));
} }
if (asyncFilterValues[columnKey] && asyncFilterValues[columnKey].length > 0) { if (asyncFilterValues[columnKey] && asyncFilterValues[columnKey].length > 0) {
@ -1125,7 +1025,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
); );
} }
return []; return [];
}, [detectedColumns, asyncFilterValues, apiEndpoint, hookData, data, fkCache]); }, [detectedColumns, asyncFilterValues, apiEndpoint, hookData, data, supportsBackendPagination]);
// Close filter dropdown when clicking outside // Close filter dropdown when clicking outside
useEffect(() => { useEffect(() => {
@ -1500,10 +1400,11 @@ export function FormGeneratorTable<T extends Record<string, any>>({
return detectedColumns.map(col => { return detectedColumns.map(col => {
let cellValue = row[col.key]; let cellValue = row[col.key];
// FK resolution if (col.displayField) {
if (col.fkSource && typeof cellValue === 'string' && cellValue.length > 0) { const displayValue = row[col.displayField];
const resolved = fkCache[col.fkSource]?.[cellValue]; if (displayValue != null && displayValue !== '') {
if (resolved) cellValue = resolved; cellValue = displayValue;
}
} }
// Timestamp formatting // Timestamp formatting
@ -1541,7 +1442,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
} finally { } finally {
setCsvExporting(false); setCsvExporting(false);
} }
}, [csvExporting, detectedColumns, apiEndpoint, currentLanguage, fkCache]); }, [csvExporting, detectedColumns, apiEndpoint, currentLanguage]);
// Check if inline editing is allowed for a column (based on RBAC permissions) // Check if inline editing is allowed for a column (based on RBAC permissions)
const canInlineEdit = useMemo(() => { const canInlineEdit = useMemo(() => {
@ -1725,6 +1626,15 @@ export function FormGeneratorTable<T extends Record<string, any>>({
return column.formatter(value, row); return column.formatter(value, row);
} }
// displayField: backend-enriched label takes priority over raw value.
// Falls back to the raw value when displayField is null/undefined (unresolved FK).
if (column.displayField) {
const displayValue = row[column.displayField];
if (displayValue != null && displayValue !== '') return String(displayValue);
if (value != null && value !== '') return `NA(${value})`;
return '-';
}
if (value === null || value === undefined) { if (value === null || value === undefined) {
return '-'; return '-';
} }
@ -1750,19 +1660,6 @@ export function FormGeneratorTable<T extends Record<string, any>>({
return renderBooleanCell(value, column, row); return renderBooleanCell(value, column, row);
} }
// FK Resolution: If column has fkSource and value is a string (UUID), resolve to display label
if (column.fkSource && typeof value === 'string' && value.length > 0) {
const resolvedLabel = resolveFkValue(value, column.fkSource);
const isLoading = fkLoading[column.fkSource];
// Show loading indicator or resolved label
if (isLoading && !fkCache[column.fkSource]?.[value]) {
return <span className={styles.fkLoading}>{value.substring(0, 8)}...</span>;
}
return resolvedLabel;
}
// Check if this is an ID or hash field that should be truncated and copyable // Check if this is an ID or hash field that should be truncated and copyable
// Do this BEFORE checking for custom formatters to ensure IDs/hashes are always copyable // Do this BEFORE checking for custom formatters to ensure IDs/hashes are always copyable
const isId = isIdField(column.key); const isId = isIdField(column.key);
@ -2098,10 +1995,10 @@ export function FormGeneratorTable<T extends Record<string, any>>({
{/* Filter icon */} {/* Filter icon */}
{filterable && column.filterable !== false && ( {filterable && column.filterable !== false && (
<button <button
className={`${styles.filterIcon} ${filters[column.key] ? styles.filterActive : ''}`} className={`${styles.filterIcon} ${column.key in filters ? styles.filterActive : ''}`}
onClick={(e) => toggleFilterDropdown(column.key, e)} onClick={(e) => toggleFilterDropdown(column.key, e)}
title={filters[column.key] title={column.key in filters
? t('Filter: {value}', { value: String(filters[column.key]) }) ? (filters[column.key] === null ? t('Filter: (Leer)') : t('Filter: {value}', { value: String(filters[column.key]) }))
: t('Zum Filtern klicken') : t('Zum Filtern klicken')
} }
> >
@ -2151,7 +2048,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
> >
<div className={styles.filterDropdownHeader}> <div className={styles.filterDropdownHeader}>
<span>{t('Filter')}: {column.label}</span> <span>{t('Filter')}: {column.label}</span>
{filters[column.key] && ( {column.key in filters && (
<button <button
className={styles.filterClearBtn} className={styles.filterClearBtn}
onClick={() => clearFilter(column.key)} onClick={() => clearFilter(column.key)}
@ -2244,11 +2141,17 @@ export function FormGeneratorTable<T extends Record<string, any>>({
return ( return (
<> <>
<div <div
className={`${styles.filterOption} ${!filters[column.key] ? styles.filterOptionSelected : ''}`} className={`${styles.filterOption} ${filters[column.key] === undefined ? styles.filterOptionSelected : ''}`}
onClick={() => clearFilter(column.key)} onClick={() => clearFilter(column.key)}
> >
({t('Alle')}) ({t('Alle')})
</div> </div>
<div
className={`${styles.filterOption} ${filters[column.key] === null ? styles.filterOptionSelected : ''}`}
onClick={() => handleFilter(column.key, _EMPTY_FILTER_SENTINEL)}
>
({t('Leer')})
</div>
{filterValuesLoading[column.key] ? ( {filterValuesLoading[column.key] ? (
<div className={styles.filterOptionMore} style={{ textAlign: 'center', padding: '8px' }}> <div className={styles.filterOptionMore} style={{ textAlign: 'center', padding: '8px' }}>
{t('Lade Filterwerte...')} {t('Lade Filterwerte...')}
@ -2259,7 +2162,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
allValues={getUniqueValuesForColumn(column.key)} allValues={getUniqueValuesForColumn(column.key)}
activeFilter={filters[column.key]} activeFilter={filters[column.key]}
onSelect={(value) => handleFilter(column.key, value)} onSelect={(value) => handleFilter(column.key, value)}
resolveLabel={column.filterLabelResolver || (column.fkSource ? (val) => fkCache[column.fkSource!]?.[val] || val : undefined)} resolveLabel={column.filterLabelResolver}
/> />
)} )}
</> </>

View file

@ -153,7 +153,7 @@ export function useAdminMandates() {
return await fetchMandateByIdApi(request, mandateId); return await fetchMandateByIdApi(request, mandateId);
}, [request]); }, [request]);
// Generate columns from attributes (including fkSource/fkDisplayField for FK resolution) // Generate columns from attributes (displayField = backend {field}Label for FK columns)
const columns = attributes.map(attr => ({ const columns = attributes.map(attr => ({
key: attr.name, key: attr.name,
label: attr.label || attr.name, label: attr.label || attr.name,
@ -164,8 +164,7 @@ export function useAdminMandates() {
width: attr.width || 150, width: attr.width || 150,
minWidth: attr.minWidth || 100, minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400, maxWidth: attr.maxWidth || 400,
fkSource: (attr as any).fkSource, // API endpoint for FK data displayField: (attr as any).displayField,
fkDisplayField: (attr as any).fkDisplayField, // Which field of FK target to display
})); }));
// Create mandate // Create mandate

View file

@ -436,10 +436,12 @@ const _DashboardTab: React.FC = () => {
try { try {
const resp = await api.get('/api/system/workflow-runs/metrics'); const resp = await api.get('/api/system/workflow-runs/metrics');
setMetrics(resp.data); setMetrics(resp.data);
} catch (e) { } catch (e: any) {
const msg = e?.response?.data?.detail || e?.message || String(e);
console.error('[automations] metrics load failed', e); console.error('[automations] metrics load failed', e);
showError(t('Metriken konnten nicht geladen werden: {msg}', { msg }));
} }
}, []); }, [showError, t]);
const _loadRuns = useCallback(async (paginationParams?: any) => { const _loadRuns = useCallback(async (paginationParams?: any) => {
if (paginationParams !== undefined) { if (paginationParams !== undefined) {
@ -543,8 +545,7 @@ const _DashboardTab: React.FC = () => {
width: 140, width: 140,
sortable: true, sortable: true,
filterable: true, filterable: true,
fkSource: '/api/mandates/', displayField: 'mandateLabel',
fkDisplayField: 'label',
}, },
{ {
key: 'featureInstanceId', key: 'featureInstanceId',
@ -553,8 +554,7 @@ const _DashboardTab: React.FC = () => {
width: 140, width: 140,
sortable: true, sortable: true,
filterable: true, filterable: true,
fkSource: '/api/features/instances', displayField: 'instanceLabel',
fkDisplayField: 'label',
}, },
{ {
key: 'status', key: 'status',
@ -818,30 +818,45 @@ const _WorkflowsTab: React.FC = () => {
const _handleExecute = useCallback(async (row: SystemWorkflow) => { const _handleExecute = useCallback(async (row: SystemWorkflow) => {
if (!row.featureInstanceId) return; if (!row.featureInstanceId) return;
setExecutingId(row.id); setExecutingId(row.id);
// Track outcome of the fire-and-forget executeGraph promise so the
// intermediate "Workflow gestartet" toast is only shown when the call has
// not already failed/finished within the 1s observation window. Without
// this we always toasted "gestartet" — even when the run had already
// errored — producing contradictory toasts and hiding real failures.
let observedFailure = false;
let observedSuccess = false;
try { try {
const invs = row.invocations || []; const invs = row.invocations || [];
const primary = const primary =
invs.find((i) => i.enabled && i.kind === 'manual') || invs.find((i) => i.enabled && i.kind === 'manual') ||
invs.find((i) => i.enabled && (i.kind === 'form' || i.kind === 'api')); invs.find((i) => i.enabled && (i.kind === 'form' || i.kind === 'api'));
const emptyGraph = { nodes: [], connections: [] }; const emptyGraph = { nodes: [], connections: [] };
executeGraph(request, row.featureInstanceId, emptyGraph as any, row.id, { const exec = executeGraph(request, row.featureInstanceId, emptyGraph as any, row.id, {
...(primary ? { entryPointId: primary.id } : {}), ...(primary ? { entryPointId: primary.id } : {}),
}).then((result) => { }).then((result) => {
if (result?.success) { if (result?.success) {
observedSuccess = true;
showSuccess(result?.paused showSuccess(result?.paused
? t('Workflow pausiert bei Human Task.') ? t('Workflow pausiert bei Human Task.')
: t('Workflow abgeschlossen')); : t('Workflow abgeschlossen'));
} else { } else {
observedFailure = true;
showError(result?.error || t('Ausführung fehlgeschlagen')); showError(result?.error || t('Ausführung fehlgeschlagen'));
} }
_load(); _load();
}).catch((e: any) => { }).catch((e: any) => {
observedFailure = true;
showError(t('Fehler: {msg}', { msg: e?.message || t('Ausführung fehlgeschlagen') })); showError(t('Fehler: {msg}', { msg: e?.message || t('Ausführung fehlgeschlagen') }));
_load(); _load();
}); });
await new Promise((r) => setTimeout(r, 1000)); await Promise.race([
exec,
new Promise((r) => setTimeout(r, 1000)),
]);
await _load(); await _load();
if (!observedFailure && !observedSuccess) {
showSuccess(t('Workflow gestartet')); showSuccess(t('Workflow gestartet'));
}
} finally { } finally {
setExecutingId(null); setExecutingId(null);
} }
@ -870,8 +885,24 @@ const _WorkflowsTab: React.FC = () => {
const _columns: ColumnConfig[] = useMemo(() => [ const _columns: ColumnConfig[] = useMemo(() => [
{ key: 'label', label: t('Workflow'), type: 'string', width: 200, sortable: true, filterable: true }, { key: 'label', label: t('Workflow'), type: 'string', width: 200, sortable: true, filterable: true },
{ key: 'mandateId', label: t('Mandant'), type: 'string', width: 140, sortable: true, filterable: true, fkSource: '/api/mandates/', fkDisplayField: 'label' }, {
{ key: 'featureInstanceId', label: t('Instanz'), type: 'string', width: 140, sortable: true, filterable: true, fkSource: '/api/features/instances', fkDisplayField: 'label' }, key: 'mandateId',
label: t('Mandant'),
type: 'string',
width: 140,
sortable: true,
filterable: true,
displayField: 'mandateLabel',
},
{
key: 'featureInstanceId',
label: t('Instanz'),
type: 'string',
width: 140,
sortable: true,
filterable: true,
displayField: 'instanceLabel',
},
{ {
key: 'active', key: 'active',
label: t('Aktiv'), label: t('Aktiv'),

View file

@ -69,8 +69,7 @@ export const AdminUsersPage: React.FC = () => {
width: attr.width || 150, width: attr.width || 150,
minWidth: attr.minWidth || 100, minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400, maxWidth: attr.maxWidth || 400,
fkSource: (attr as any).fkSource, displayField: (attr as any).displayField,
fkDisplayField: (attr as any).fkDisplayField,
})); }));
}, [attributes]); }, [attributes]);

View file

@ -67,15 +67,12 @@ export const ConnectionsPage: React.FC = () => {
width: attr.width || 150, width: attr.width || 150,
minWidth: attr.minWidth || 100, minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400, maxWidth: attr.maxWidth || 400,
fkSource: (attr as any).fkSource, displayField: (attr as any).displayField,
fkDisplayField: (attr as any).fkDisplayField,
frontendFormat: (attr as any).frontendFormat, frontendFormat: (attr as any).frontendFormat,
frontendFormatLabels: (attr as any).frontendFormatLabels, frontendFormatLabels: (attr as any).frontendFormatLabels,
}; };
if (attr.name === 'userId') { if (attr.name === 'userId') {
col.fkSource = '/api/users/';
col.fkDisplayField = 'username';
col.label = t('Benutzer'); col.label = t('Benutzer');
} }

View file

@ -210,8 +210,7 @@ export const FilesPage: React.FC = () => {
width: attr.width || 150, width: attr.width || 150,
minWidth: attr.minWidth || 100, minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400, maxWidth: attr.maxWidth || 400,
fkSource: (attr as any).fkSource, displayField: (attr as any).displayField,
fkDisplayField: (attr as any).fkDisplayField,
frontendFormat: (attr as any).frontendFormat, frontendFormat: (attr as any).frontendFormat,
frontendFormatLabels: (attr as any).frontendFormatLabels, frontendFormatLabels: (attr as any).frontendFormatLabels,
})); }));
@ -225,8 +224,7 @@ export const FilesPage: React.FC = () => {
width: 150, width: 150,
minWidth: 100, minWidth: 100,
maxWidth: 250, maxWidth: 250,
fkSource: '/api/users/', displayField: 'sysCreatedByLabel',
fkDisplayField: 'username',
} as any); } as any);
return cols; return cols;
}, [attributes, t]); }, [attributes, t]);

View file

@ -83,8 +83,7 @@ export const PromptsPage: React.FC = () => {
width: attr.name === 'content' ? 300 : attr.width || 150, width: attr.name === 'content' ? 300 : attr.width || 150,
minWidth: attr.minWidth || 100, minWidth: attr.minWidth || 100,
maxWidth: attr.name === 'content' ? 500 : attr.maxWidth || 400, maxWidth: attr.name === 'content' ? 500 : attr.maxWidth || 400,
fkSource: (attr as any).fkSource, displayField: (attr as any).displayField,
fkDisplayField: (attr as any).fkDisplayField,
frontendFormat: (attr as any).frontendFormat, frontendFormat: (attr as any).frontendFormat,
frontendFormatLabels: (attr as any).frontendFormatLabels, frontendFormatLabels: (attr as any).frontendFormatLabels,
})); }));
@ -100,8 +99,7 @@ export const PromptsPage: React.FC = () => {
width: 150, width: 150,
minWidth: 100, minWidth: 100,
maxWidth: 250, maxWidth: 250,
fkSource: '/api/users/', displayField: 'sysCreatedByLabel',
fkDisplayField: 'username',
frontendFormat: undefined, frontendFormat: undefined,
frontendFormatLabels: undefined, frontendFormatLabels: undefined,
}); });

View file

@ -4,6 +4,10 @@
* Keeps the CommCoach dossier/coaching page mounted across route changes. * Keeps the CommCoach dossier/coaching page mounted across route changes.
* Visibility is toggled via CSS so session state, messages, and input state * Visibility is toggled via CSS so session state, messages, and input state
* stay alive when the user leaves and later returns. * stay alive when the user leaves and later returns.
*
* Persistence is scoped per `(mandateId, instanceId)` switching to a
* different mandate or instance via the navigator unmounts the previous
* view and mounts a fresh one.
*/ */
import React, { useRef } from 'react'; import React, { useRef } from 'react';
@ -30,6 +34,7 @@ export const CommcoachKeepAlive: React.FC<CommcoachKeepAliveProps> = ({ isVisibl
const mandateId = cachedMandateIdRef.current; const mandateId = cachedMandateIdRef.current;
const instanceId = cachedInstanceIdRef.current; const instanceId = cachedInstanceIdRef.current;
if (!mandateId || !instanceId) return null; if (!mandateId || !instanceId) return null;
const scopeKey = `${mandateId}:${instanceId}`;
return ( return (
<div <div
@ -44,6 +49,7 @@ export const CommcoachKeepAlive: React.FC<CommcoachKeepAliveProps> = ({ isVisibl
}} }}
> >
<CommcoachDossierView <CommcoachDossierView
key={scopeKey}
persistentInstanceId={instanceId} persistentInstanceId={instanceId}
persistentMandateId={mandateId} persistentMandateId={mandateId}
/> />

View file

@ -0,0 +1,96 @@
// Copyright (c) 2025 Patrick Motsch
// All rights reserved.
//
// Persistence is per (mandateId, instanceId): switching to a different mandate
// or instance must remount the editor page so its internal state (loaded
// workflow, currentWorkflowId, …) is reset and saves go to the right tenant.
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { render, screen, act } from '@testing-library/react';
import { MemoryRouter, useNavigate } from 'react-router-dom';
const _mountCount = { value: 0 };
vi.mock('./GraphicalEditorPage', () => ({
GraphicalEditorPage: ({ persistentMandateId, persistentInstanceId }: { persistentMandateId?: string; persistentInstanceId?: string }) => {
React.useEffect(() => {
_mountCount.value += 1;
}, []);
return <div data-testid="ge-page">{persistentMandateId}::{persistentInstanceId}</div>;
},
}));
import { GraphicalEditorKeepAlive } from './GraphicalEditorKeepAlive';
let _navigateTo: ((path: string) => void) | null = null;
const _NavCapture: React.FC = () => {
_navigateTo = useNavigate();
return null;
};
function _renderHarness(initialPath: string) {
return render(
<MemoryRouter initialEntries={[initialPath]}>
<_NavCapture />
<GraphicalEditorKeepAlive isVisible />
</MemoryRouter>,
);
}
function _navigate(path: string) {
act(() => {
_navigateTo?.(path);
});
}
describe('GraphicalEditorKeepAlive — persistence per (mandate, instance)', () => {
it('remounts the page when the mandate changes', () => {
_mountCount.value = 0;
_renderHarness('/mandates/mA/graphicalEditor/iA/editor');
expect(_mountCount.value).toBe(1);
expect(screen.getByTestId('ge-page').textContent).toBe('mA::iA');
_navigate('/mandates/mB/graphicalEditor/iA/editor');
expect(_mountCount.value).toBe(2);
expect(screen.getByTestId('ge-page').textContent).toBe('mB::iA');
});
it('remounts the page when the instance changes', () => {
_mountCount.value = 0;
_renderHarness('/mandates/mA/graphicalEditor/iA/editor');
expect(_mountCount.value).toBe(1);
_navigate('/mandates/mA/graphicalEditor/iZ/editor');
expect(_mountCount.value).toBe(2);
expect(screen.getByTestId('ge-page').textContent).toBe('mA::iZ');
});
it('does NOT remount when the route stays on the same (mandate, instance)', () => {
_mountCount.value = 0;
_renderHarness('/mandates/mA/graphicalEditor/iA/editor');
expect(_mountCount.value).toBe(1);
_navigate('/mandates/mA/graphicalEditor/iA/editor');
expect(_mountCount.value).toBe(1);
});
it('keeps the cached page mounted (no remount) when the user navigates AWAY and BACK to the same scope', () => {
_mountCount.value = 0;
_renderHarness('/mandates/mA/graphicalEditor/iA/editor');
expect(_mountCount.value).toBe(1);
// Away to a non-editor route: the regex match fails, refs keep their
// previous values — the cached page must not remount.
_navigate('/admin/languages');
expect(_mountCount.value).toBe(1);
expect(screen.getByTestId('ge-page').textContent).toBe('mA::iA');
// Back to the same (mandate, instance) — still no remount.
_navigate('/mandates/mA/graphicalEditor/iA/editor');
expect(_mountCount.value).toBe(1);
});
});

View file

@ -4,9 +4,16 @@
* Keeps the GraphicalEditorPage mounted across route changes so the canvas * Keeps the GraphicalEditorPage mounted across route changes so the canvas
* state, SSE connections, and editor context survive navigation to ANY page * state, SSE connections, and editor context survive navigation to ANY page
* (other features, admin, settings, etc.). * (other features, admin, settings, etc.).
* Visibility is toggled via CSS `display` instead of mount / unmount. *
* Cached mandateId/instanceId are passed as props so the page does not * Persistence is scoped per `(mandateId, instanceId)`: when the user switches
* depend on URL params (which disappear on non-feature routes). * to a DIFFERENT mandate or instance via the navigator, the previous editor
* mount is discarded and a fresh page is mounted. Otherwise stale state from
* mandate A leaks into mandate B and saves end up hitting the wrong tenant
* (HTTP 404 / "not found").
*
* Implementation: feeds the cached `(mandate, instance)` tuple into both
* `props` and `key`. React reuses the mount as long as the tuple stays
* identical and unmounts/remounts on change.
*/ */
import React, { useRef } from 'react'; import React, { useRef } from 'react';
@ -34,6 +41,10 @@ export const GraphicalEditorKeepAlive: React.FC<GraphicalEditorKeepAliveProps> =
if (!hasEverMountedRef.current) return null; if (!hasEverMountedRef.current) return null;
const mandateId = cachedMandateIdRef.current;
const instanceId = cachedInstanceIdRef.current;
const scopeKey = `${mandateId}:${instanceId}`;
return ( return (
<div <div
style={{ style={{
@ -48,8 +59,9 @@ export const GraphicalEditorKeepAlive: React.FC<GraphicalEditorKeepAliveProps> =
}} }}
> >
<GraphicalEditorPage <GraphicalEditorPage
persistentInstanceId={cachedInstanceIdRef.current} key={scopeKey}
persistentMandateId={cachedMandateIdRef.current} persistentInstanceId={instanceId}
persistentMandateId={mandateId}
/> />
</div> </div>
); );

View file

@ -200,8 +200,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
label: t('Erstellt von'), label: t('Erstellt von'),
type: 'string', type: 'string',
width: 140, width: 140,
fkSource: '/api/users/', displayField: 'sysCreatedByLabel',
fkDisplayField: 'username',
}, },
{ {
key: 'sysCreatedAt', key: 'sysCreatedAt',

View file

@ -50,8 +50,6 @@ export const TrusteePositionDocumentsView: React.FC = () => {
} }
}, [instanceId]); }, [instanceId]);
// Generate columns from attributes (like TrusteePositionsView)
// Map frontend_options to fkSource for FK resolution
const columns = useMemo(() => { const columns = useMemo(() => {
if (!attributes || attributes.length === 0) return []; if (!attributes || attributes.length === 0) return [];
@ -60,14 +58,7 @@ export const TrusteePositionDocumentsView: React.FC = () => {
return attributes return attributes
.filter((attr: any) => !excludedFields.includes(attr.name)) .filter((attr: any) => !excludedFields.includes(attr.name))
.map((attr: any) => { .map((attr: any) => ({
// Replace {instanceId} placeholder in options URL
let fkSource = attr.options;
if (typeof fkSource === 'string' && instanceId) {
fkSource = fkSource.replace('{instanceId}', instanceId);
}
return {
key: attr.name, key: attr.name,
label: attr.label || attr.name, label: attr.label || attr.name,
type: attr.type as any, type: attr.type as any,
@ -77,12 +68,9 @@ export const TrusteePositionDocumentsView: React.FC = () => {
width: attr.width || 200, width: attr.width || 200,
minWidth: attr.minWidth || 100, minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400, maxWidth: attr.maxWidth || 400,
// Use frontend_options as fkSource for FK resolution displayField: attr.displayField,
fkSource: typeof fkSource === 'string' ? fkSource : undefined, }));
fkDisplayField: 'label', }, [attributes]);
};
});
}, [attributes, instanceId]);
// Check permissions (general level) // Check permissions (general level)
// Row-level permissions are handled automatically by FormGeneratorTable // Row-level permissions are handled automatically by FormGeneratorTable

View file

@ -129,8 +129,7 @@ export const TrusteeDataTab: React.FC<TrusteeDataTabProps> = ({
width: attr.width || 150, width: attr.width || 150,
minWidth: attr.minWidth || 100, minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400, maxWidth: attr.maxWidth || 400,
fkSource: attr.fkSource, displayField: attr.displayField,
fkDisplayField: attr.fkDisplayField,
frontendFormat: attr.frontendFormat, frontendFormat: attr.frontendFormat,
frontendFormatLabels: attr.frontendFormatLabels, frontendFormatLabels: attr.frontendFormatLabels,
})); }));

View file

@ -5,6 +5,11 @@
* survives route changes. Visibility is toggled via CSS `display` * survives route changes. Visibility is toggled via CSS `display`
* instead of mount / unmount, preserving messages, SSE connections, * instead of mount / unmount, preserving messages, SSE connections,
* files, and all other workspace state. * files, and all other workspace state.
*
* Persistence is scoped per `(mandateId, instanceId)` switching to a
* different mandate or instance via the navigator unmounts the previous
* page and mounts a fresh one (otherwise stale state from tenant A
* leaks into tenant B).
*/ */
import React, { useRef } from 'react'; import React, { useRef } from 'react';
@ -19,15 +24,19 @@ interface WorkspaceKeepAliveProps {
export const WorkspaceKeepAlive: React.FC<WorkspaceKeepAliveProps> = ({ isVisible }) => { export const WorkspaceKeepAlive: React.FC<WorkspaceKeepAliveProps> = ({ isVisible }) => {
const location = useLocation(); const location = useLocation();
const cachedMandateIdRef = useRef<string>('');
const cachedInstanceIdRef = useRef<string>(''); const cachedInstanceIdRef = useRef<string>('');
const match = location.pathname.match(_WORKSPACE_ROUTE_RE); const match = location.pathname.match(_WORKSPACE_ROUTE_RE);
if (match?.[2]) { if (match?.[1] && match?.[2]) {
cachedMandateIdRef.current = match[1];
cachedInstanceIdRef.current = match[2]; cachedInstanceIdRef.current = match[2];
} }
const mandateId = cachedMandateIdRef.current;
const instanceId = cachedInstanceIdRef.current; const instanceId = cachedInstanceIdRef.current;
if (!instanceId) return null; if (!instanceId) return null;
const scopeKey = `${mandateId}:${instanceId}`;
return ( return (
<div <div
@ -42,7 +51,7 @@ export const WorkspaceKeepAlive: React.FC<WorkspaceKeepAliveProps> = ({ isVisibl
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<WorkspacePage persistentInstanceId={instanceId} /> <WorkspacePage key={scopeKey} persistentInstanceId={instanceId} />
</div> </div>
); );
}; };

View file

@ -1,489 +0,0 @@
src/components/FlowEditor/nodes/configs/CommentNodeConfig.tsx(8,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/configs/CommentNodeConfig.tsx(17,22): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/ConfirmationNodeConfig.tsx(8,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/configs/ConfirmationNodeConfig.tsx(17,22): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/ConfirmationNodeConfig.tsx(21,15): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(9,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(61,39): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(61,70): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(78,72): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(78,114): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(96,48): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(97,51): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(98,48): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(115,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(119,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(123,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(127,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(131,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(135,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(139,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(143,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(147,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(151,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(161,51): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(176,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(180,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(184,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(188,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(198,45): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(217,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(225,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(230,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/EmailNodeConfig.tsx(234,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/FileCreateNodeConfig.tsx(12,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/configs/FileCreateNodeConfig.tsx(46,17): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/FileCreateNodeConfig.tsx(52,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/FileCreateNodeConfig.tsx(58,22): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/FileCreateNodeConfig.tsx(59,27): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/FileCreateNodeConfig.tsx(96,17): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/FileCreateNodeConfig.tsx(109,17): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(13,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(157,50): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(157,86): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(168,19): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(179,19): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(194,19): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(195,19): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(213,19): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(217,26): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(225,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/SharePointNodeConfig.tsx(233,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(10,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(36,17): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(40,24): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(47,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(53,43): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(53,74): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(62,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(70,21): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(74,28): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(83,19): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/TrusteeNodeConfig.tsx(87,26): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/UploadNodeConfig.tsx(11,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/configs/UploadNodeConfig.tsx(42,17): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/configs/UploadNodeConfig.tsx(60,17): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/form/FormNodeConfig.tsx(11,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/form/FormNodeConfig.tsx(78,24): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/form/FormNodeConfig.tsx(135,48): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/form/FormNodeConfig.tsx(136,49): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/form/FormNodeConfig.tsx(153,24): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/form/FormNodeConfig.tsx(200,32): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(28,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(157,13): error TS2345: Argument of type 'string' is not assignable to parameter of type 'ApiRequestOptions<any>'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(172,27): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(216,34): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(218,33): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(219,33): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(225,141): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(260,142): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(284,140): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(296,20): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(299,68): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(312,32): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(313,31): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(314,31): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(316,34): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(317,38): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(318,36): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(319,37): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(341,43): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(343,43): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(347,144): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(362,32): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(364,39): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(365,36): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(366,39): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(367,31): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx(368,31): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx(15,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx(102,78): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx(122,33): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx(144,25): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx(145,25): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/shared/DataPicker.tsx(14,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/shared/DataPicker.tsx(142,51): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/shared/DataPicker.tsx(143,98): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/shared/DataPicker.tsx(184,61): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/shared/DataPicker.tsx(192,43): error TS2345: Argument of type 'CanvasConnection[]' is not assignable to parameter of type '{ source: string; target: string; sourceOutput?: number | undefined; }[]'.
Type 'CanvasConnection' is missing the following properties from type '{ source: string; target: string; sourceOutput?: number | undefined; }': source, target
src/components/FlowEditor/nodes/shared/DynamicValueField.tsx(17,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/shared/DynamicValueField.tsx(59,54): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/shared/DynamicValueField.tsx(104,26): error TS2345: Argument of type 'DataRef | SystemVarRef' is not assignable to parameter of type 'DataRef | null'.
Type 'SystemVarRef' is missing the following properties from type 'DataRef': nodeId, path
src/components/FlowEditor/nodes/shared/HybridStaticRefField.tsx(16,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/shared/HybridStaticRefField.tsx(87,26): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/shared/LoopItemsSelect.tsx(12,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/shared/LoopItemsSelect.tsx(186,15): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(15,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(116,29): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(156,29): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(157,33): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(158,34): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(191,24): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(197,19): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(202,26): error TS2304: Cannot find name 't'.
src/components/FlowEditor/nodes/switch/SwitchNodeConfig.tsx(208,17): error TS2304: Cannot find name 't'.
src/components/NotificationBell/NotificationBell.tsx(13,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/NotificationBell/NotificationBell.tsx(161,18): error TS2304: Cannot find name 't'.
src/components/NotificationBell/NotificationBell.tsx(175,48): error TS2304: Cannot find name 't'.
src/components/NotificationBell/NotificationBell.tsx(185,21): error TS2304: Cannot find name 't'.
src/components/NotificationBell/NotificationBell.tsx(255,33): error TS2304: Cannot find name 't'.
src/components/OnboardingWizard.tsx(4,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/OnboardingWizard.tsx(26,18): error TS2304: Cannot find name 't'.
src/components/OnboardingWizard.tsx(48,64): error TS2304: Cannot find name 't'.
src/components/OnboardingWizard.tsx(62,24): error TS2304: Cannot find name 't'.
src/components/OnboardingWizard.tsx(77,24): error TS2304: Cannot find name 't'.
src/components/OnboardingWizard.tsx(92,26): error TS2304: Cannot find name 't'.
src/components/OnboardingWizard.tsx(116,24): error TS2304: Cannot find name 't'.
src/components/OnboardingWizard.tsx(116,65): error TS2304: Cannot find name 't'.
src/components/ProviderSelector/ProviderSelector.tsx(21,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/ProviderSelector/ProviderSelector.tsx(145,27): error TS2304: Cannot find name 't'.
src/components/ProviderSelector/ProviderSelector.tsx(283,16): error TS2304: Cannot find name 't'.
src/components/ProviderSelector/ProviderSelector.tsx(304,46): error TS2304: Cannot find name 't'.
src/components/ProviderSelector/ProviderSelector.tsx(348,51): error TS2304: Cannot find name 't'.
src/components/RbacExportImport/RbacExportImport.tsx(90,53): error TS2304: Cannot find name 't'.
src/components/UiComponents/AddressAutocomplete/AddressAutocomplete.tsx(7,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UiComponents/AddressAutocomplete/AddressAutocomplete.tsx(278,55): error TS2304: Cannot find name 't'.
src/components/UiComponents/AddressAutocomplete/AddressAutocomplete.tsx(288,57): error TS2304: Cannot find name 't'.
src/components/UiComponents/AutoScroll/AutoScroll.tsx(4,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UiComponents/AutoScroll/AutoScroll.tsx(152,23): error TS2304: Cannot find name 't'.
src/components/UiComponents/BauvorschriftenSection/BauvorschriftenSection.tsx(5,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UiComponents/BauvorschriftenSection/BauvorschriftenSection.tsx(44,49): error TS2304: Cannot find name 't'.
src/components/UiComponents/BauvorschriftenSection/BauvorschriftenSection.tsx(56,49): error TS2304: Cannot find name 't'.
src/components/UiComponents/BauvorschriftenSection/BauvorschriftenSection.tsx(68,49): error TS2304: Cannot find name 't'.
src/components/UiComponents/BauvorschriftenSection/BauvorschriftenSection.tsx(74,49): error TS2304: Cannot find name 't'.
src/components/UiComponents/BauvorschriftenSection/BauvorschriftenSection.tsx(80,49): error TS2304: Cannot find name 't'.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(15,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(186,39): error TS2304: Cannot find name 't'.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(233,52): error TS2304: Cannot find name 't'.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(233,100): error TS2304: Cannot find name 't'.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(240,42): error TS2304: Cannot find name 't'.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(240,82): error TS2304: Cannot find name 't'.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(250,60): error TS2304: Cannot find name 't'.
src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx(250,95): error TS2304: Cannot find name 't'.
src/components/UiComponents/MapView/MapViewLeaflet.tsx(28,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UiComponents/MapView/MapViewLeaflet.tsx(196,47): error TS2304: Cannot find name 't'.
src/components/UiComponents/MapView/MapViewLeaflet.tsx(196,107): error TS2304: Cannot find name 't'.
src/components/UiComponents/MapView/MapViewLeaflet.tsx(209,45): error TS2304: Cannot find name 't'.
src/components/UiComponents/MapView/MapViewLeaflet.tsx(209,105): error TS2304: Cannot find name 't'.
src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx(12,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx(83,20): error TS2304: Cannot find name 't'.
src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx(203,28): error TS2304: Cannot find name 't'.
src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx(211,28): error TS2304: Cannot find name 't'.
src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx(221,26): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(10,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(229,59): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(268,34): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(319,59): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(332,59): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(346,60): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(364,44): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(382,38): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(406,73): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(423,73): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(442,73): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(493,74): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(534,34): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(585,59): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(598,59): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(612,60): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(630,44): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(648,38): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(672,73): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(689,73): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(708,73): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(757,74): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(844,47): error TS2304: Cannot find name 't'.
src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx(1083,47): error TS2304: Cannot find name 't'.
src/components/UiComponents/Toast/Toast.tsx(23,11): error TS6133: 't' is declared but its value is never read.
src/components/UiComponents/Toast/Toast.tsx(57,21): error TS2304: Cannot find name 't'.
src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx(36,11): error TS6133: 't' is declared but its value is never read.
src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx(73,55): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/ChatsTab.tsx(6,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UnifiedDataBar/ChatsTab.tsx(311,26): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/ChatsTab.tsx(333,56): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/ChatsTab.tsx(341,24): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/ChatsTab.tsx(346,119): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/ChatsTab.tsx(438,36): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/ChatsTab.tsx(438,75): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/FilesTab.tsx(9,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UnifiedDataBar/FilesTab.tsx(235,56): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/FilesTab.tsx(263,20): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/FilesTab.tsx(286,22): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/FilesTab.tsx(327,28): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/FilesTab.tsx(327,65): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(23,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/components/UnifiedDataBar/SourcesTab.tsx(855,42): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(855,90): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(862,26): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(958,32): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(988,53): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(988,84): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(995,36): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1031,49): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1031,80): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1038,32): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1168,20): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1174,82): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1431,18): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1437,80): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1577,20): error TS2304: Cannot find name 't'.
src/components/UnifiedDataBar/SourcesTab.tsx(1583,82): error TS2304: Cannot find name 't'.
src/hooks/usePlayground.ts(2,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'Workflow'.
src/hooks/usePlayground.ts(3,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'WorkflowMessage'.
src/hooks/usePlayground.ts(4,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'WorkflowLog'.
src/hooks/usePlayground.ts(5,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'StartWorkflowRequest'.
src/hooks/usePlayground.ts(6,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'StartWorkflowResponse'.
src/hooks/usePlayground.ts(7,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'ChatDataResponse'.
src/hooks/useWorkflows.ts(4,3): error TS2724: '"../api/workflowApi"' has no exported member named 'deleteWorkflowApi'. Did you mean 'deleteWorkflow'?
src/hooks/useWorkflows.ts(5,3): error TS2724: '"../api/workflowApi"' has no exported member named 'deleteWorkflowsApi'. Did you mean 'deleteWorkflow'?
src/hooks/useWorkflows.ts(6,3): error TS2724: '"../api/workflowApi"' has no exported member named 'updateWorkflowApi'. Did you mean 'updateWorkflow'?
src/hooks/useWorkflows.ts(9,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'fetchAttributes'.
src/hooks/useWorkflows.ts(10,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'startWorkflowApi'.
src/hooks/useWorkflows.ts(11,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'stopWorkflowApi'.
src/hooks/useWorkflows.ts(12,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'deleteMessageApi'.
src/hooks/useWorkflows.ts(13,3): error TS2305: Module '"../api/workflowApi"' has no exported member 'deleteFileFromMessageApi'.
src/hooks/useWorkflows.ts(14,8): error TS2305: Module '"../api/workflowApi"' has no exported member 'Workflow'.
src/hooks/useWorkflows.ts(15,8): error TS2305: Module '"../api/workflowApi"' has no exported member 'AttributeDefinition'.
src/hooks/useWorkflows.ts(16,8): error TS2305: Module '"../api/workflowApi"' has no exported member 'StartWorkflowRequest'.
src/hooks/useWorkflows.ts(32,15): error TS2305: Module '"../api/workflowApi"' has no exported member 'AttributeDefinition'.
src/hooks/useWorkflows.ts(132,49): error TS2345: Argument of type 'undefined' is not assignable to parameter of type 'string'.
src/hooks/useWorkflows.ts(195,72): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
Type 'undefined' is not assignable to type 'string'.
src/hooks/useWorkflows.ts(196,14): error TS2352: Conversion of type 'Automation2Workflow' to type 'UserWorkflow' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Type 'Automation2Workflow' is missing the following properties from type 'UserWorkflow': mandateId, status
src/hooks/useWorkflows.ts(247,40): error TS7006: Parameter 'opt' implicitly has an 'any' type.
src/hooks/useWorkflows.ts(263,40): error TS7006: Parameter 'opt' implicitly has an 'any' type.
src/layouts/FeatureLayout.tsx(23,9): error TS2304: Cannot find name 't'.
src/layouts/FeatureLayout.tsx(39,10): error TS2304: Cannot find name 't'.
src/layouts/FeatureLayout.tsx(62,11): error TS6133: 't' is declared but its value is never read.
src/pages/admin/AdminFeatureInstanceUsersPage.tsx(13,26): error TS6133: 'FaUsers' is declared but its value is never read.
src/pages/admin/AdminInvitationsPage.tsx(13,26): error TS6133: 'FaEnvelopeOpenText' is declared but its value is never read.
src/pages/admin/AdminMandatesPage.tsx(20,26): error TS6133: 'FaBuilding' is declared but its value is never read.
src/pages/admin/AdminUserMandatesPage.tsx(12,26): error TS6133: 'FaUsers' is declared but its value is never read.
src/pages/admin/AdminUsersPage.tsx(12,26): error TS6133: 'FaUsers' is declared but its value is never read.
src/pages/basedata/ConnectionsPage.tsx(12,18): error TS6133: 'FaPlug' is declared but its value is never read.
src/pages/basedata/FilesPage.tsx(17,18): error TS6133: 'FaFolder' is declared but its value is never read.
src/pages/billing/BillingDashboard.tsx(12,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/billing/BillingDashboard.tsx(69,56): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(73,44): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(87,14): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(89,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(109,14): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(111,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(131,14): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(133,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(189,14): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(190,41): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(197,46): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(199,55): error TS2304: Cannot find name 't'.
src/pages/billing/BillingDashboard.tsx(201,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(19,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/billing/BillingMandateView.tsx(47,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(48,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(49,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(72,62): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(72,89): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(134,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(136,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(140,49): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(218,45): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(229,55): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(231,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(254,55): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(256,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(268,30): error TS2304: Cannot find name 't'.
src/pages/billing/BillingMandateView.tsx(268,58): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(12,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/billing/BillingTransactions.tsx(98,14): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(99,41): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(106,55): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(108,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(116,26): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(118,26): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(122,57): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(140,30): error TS2304: Cannot find name 't'.
src/pages/billing/BillingTransactions.tsx(140,63): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(20,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/billing/BillingUserView.tsx(93,31): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(108,31): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(121,20): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(122,20): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(123,51): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(125,20): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(144,70): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(227,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(228,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(230,18): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(234,49): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(314,41): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(323,55): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(325,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(350,55): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(352,43): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(368,30): error TS2304: Cannot find name 't'.
src/pages/billing/BillingUserView.tsx(368,55): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(413,11): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(414,11): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(416,39): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(416,79): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(417,46): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(417,81): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(418,45): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(418,80): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(448,56): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(475,46): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(481,29): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(481,75): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(499,48): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(503,20): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(513,46): error TS2304: Cannot find name 't'.
src/pages/billing/SubscriptionTab.tsx(515,43): error TS2304: Cannot find name 't'.
src/pages/Dashboard.tsx(18,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/Dashboard.tsx(74,16): error TS2304: Cannot find name 't'.
src/pages/Dashboard.tsx(75,43): error TS2304: Cannot find name 't'.
src/pages/Dashboard.tsx(84,14): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(54,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/FeatureView.tsx(69,27): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(69,72): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(73,46): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(77,27): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(84,27): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(84,75): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(90,10): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(91,9): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(97,10): error TS2304: Cannot find name 't'.
src/pages/FeatureView.tsx(98,9): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(29,11): error TS6133: 't' is declared but its value is never read.
src/pages/GDPR.tsx(165,48): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(168,20): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(169,19): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(190,20): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(191,19): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(212,20): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(213,19): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(282,48): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(283,65): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(288,22): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(298,22): error TS2304: Cannot find name 't'.
src/pages/GDPR.tsx(308,22): error TS2304: Cannot find name 't'.
src/pages/Register.tsx(47,11): error TS6133: 't' is declared but its value is never read.
src/pages/Register.tsx(142,26): error TS2304: Cannot find name 't'.
src/pages/Register.tsx(195,115): error TS2304: Cannot find name 't'.
src/pages/Register.tsx(199,25): error TS2304: Cannot find name 't'.
src/pages/Register.tsx(219,24): error TS2304: Cannot find name 't'.
src/pages/Reset.tsx(40,11): error TS6133: 't' is declared but its value is never read.
src/pages/Reset.tsx(111,45): error TS2304: Cannot find name 't'.
src/pages/Reset.tsx(123,26): error TS2304: Cannot find name 't'.
src/pages/Reset.tsx(151,43): error TS2304: Cannot find name 't'.
src/pages/Reset.tsx(163,57): error TS2304: Cannot find name 't'.
src/pages/Reset.tsx(178,106): error TS2304: Cannot find name 't'.
src/pages/Reset.tsx(196,120): error TS2304: Cannot find name 't'.
src/pages/Reset.tsx(210,24): error TS2304: Cannot find name 't'.
src/pages/Store.tsx(140,38): error TS2304: Cannot find name 't'.
src/pages/Store.tsx(140,65): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(32,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/views/commcoach/CommcoachDossierView.tsx(252,46): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(263,35): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(263,82): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(295,18): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(306,26): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(314,26): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(320,41): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(323,43): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(326,47): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(330,53): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(330,90): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(332,95): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(340,16): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(341,15): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(342,90): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(358,162): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(359,163): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(363,54): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(363,93): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(376,38): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(388,21): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(391,61): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(419,58): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(433,44): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(433,91): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(435,38): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(435,74): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(438,63): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(438,105): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(441,63): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(441,103): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(490,86): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(490,131): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(603,36): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(617,34): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(642,148): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(672,34): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(699,152): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(776,30): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(782,57): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(782,98): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(786,49): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(815,49): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(822,95): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(822,125): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(832,47): error TS2304: Cannot find name 't'.
src/pages/views/commcoach/CommcoachDossierView.tsx(850,49): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(120,29): error TS2339: Property 'filter' does not exist on type 'Automation2Workflow[] | { items: Automation2Workflow[]; pagination: any; }'.
Property 'filter' does not exist on type '{ items: Automation2Workflow[]; pagination: any; }'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(121,14): error TS7006: Parameter 'w' implicitly has an 'any' type.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(376,47): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(391,38): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(476,83): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(486,29): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(600,39): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(634,22): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(648,33): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(648,83): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(855,41): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(855,94): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(871,29): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(871,79): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(879,17): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(920,47): error TS2304: Cannot find name 't'.
src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx(926,47): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(368,58): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(368,110): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(485,44): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(492,19): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(507,31): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(507,64): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(527,40): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(533,26): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(557,19): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(567,32): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(574,46): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(574,85): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(588,32): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(595,46): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(595,85): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(614,39): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(614,72): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(621,48): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(657,46): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(667,35): error TS2304: Cannot find name 't'.
src/pages/views/neutralization/NeutralizationView.tsx(682,50): error TS2304: Cannot find name 't'.
src/pages/views/workspace/ChatStream.tsx(16,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/views/workspace/ChatStream.tsx(377,85): error TS2304: Cannot find name 't'.
src/pages/views/workspace/FilePreview.tsx(16,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/views/workspace/FilePreview.tsx(102,91): error TS2304: Cannot find name 't'.
src/pages/views/workspace/NeutralizationPanel.tsx(4,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/views/workspace/NeutralizationPanel.tsx(295,92): error TS2304: Cannot find name 't'.
src/pages/views/workspace/NeutralizationPanel.tsx(439,26): error TS2304: Cannot find name 't'.
src/pages/views/workspace/NeutralizationPanel.tsx(452,15): error TS2304: Cannot find name 't'.
src/pages/views/workspace/NeutralizationPanel.tsx(453,15): error TS2304: Cannot find name 't'.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(11,1): error TS6133: 'useLanguage' is declared but its value is never read.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(69,18): error TS2304: Cannot find name 't'.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(83,18): error TS2304: Cannot find name 't'.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(97,45): error TS2304: Cannot find name 't'.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(104,39): error TS2304: Cannot find name 't'.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(132,24): error TS2304: Cannot find name 't'.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(153,19): error TS2304: Cannot find name 't'.
src/pages/views/workspace/WorkspaceGeneralSettings.tsx(153,61): error TS2304: Cannot find name 't'.