fixed language logic items
This commit is contained in:
parent
fbfe85f225
commit
d1f0b3c3d6
155 changed files with 2091 additions and 2057 deletions
|
|
@ -18,7 +18,7 @@ export interface AttributeDefinition {
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
default?: any;
|
default?: any;
|
||||||
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
|
options?: Array<{ value: string | number; label: string }> | string;
|
||||||
validation?: any;
|
validation?: any;
|
||||||
ui?: any;
|
ui?: any;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export interface Prompt {
|
||||||
|
|
||||||
export interface AttributeOption {
|
export interface AttributeOption {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string | { [key: string]: string };
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttributeDefinition {
|
export interface AttributeDefinition {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export interface AttributeDefinition {
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
default?: any;
|
default?: any;
|
||||||
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
|
options?: Array<{ value: string | number; label: string }> | string;
|
||||||
validation?: any;
|
validation?: any;
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
||||||
<button
|
<button
|
||||||
className={`${styles.iconButton} ${styles.danger}`}
|
className={`${styles.iconButton} ${styles.danger}`}
|
||||||
onClick={() => onDelete(rule.id)}
|
onClick={() => onDelete(rule.id)}
|
||||||
title={t('delete rule')}
|
title={t('Regel löschen')}
|
||||||
>
|
>
|
||||||
<FaTrash />
|
<FaTrash />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -97,7 +97,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
||||||
<div className={styles.permissionsGrid}>
|
<div className={styles.permissionsGrid}>
|
||||||
{/* View Toggle */}
|
{/* View Toggle */}
|
||||||
<div className={styles.permissionItem}>
|
<div className={styles.permissionItem}>
|
||||||
<span className={styles.permissionLabel}>View</span>
|
<span className={styles.permissionLabel}>{t('Ansicht')}</span>
|
||||||
<div className={styles.viewToggle}>
|
<div className={styles.viewToggle}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -113,7 +113,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
||||||
{isDataRule ? (
|
{isDataRule ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.permissionItem}>
|
<div className={styles.permissionItem}>
|
||||||
<span className={styles.permissionLabel}>Read</span>
|
<span className={styles.permissionLabel}>{t('Lesen')}</span>
|
||||||
<AccessLevelSelect
|
<AccessLevelSelect
|
||||||
value={rule.read}
|
value={rule.read}
|
||||||
onChange={(value) => onUpdate(rule.id, { read: value })}
|
onChange={(value) => onUpdate(rule.id, { read: value })}
|
||||||
|
|
@ -122,7 +122,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.permissionItem}>
|
<div className={styles.permissionItem}>
|
||||||
<span className={styles.permissionLabel}>Create</span>
|
<span className={styles.permissionLabel}>{t('Erstellen')}</span>
|
||||||
<AccessLevelSelect
|
<AccessLevelSelect
|
||||||
value={rule.create}
|
value={rule.create}
|
||||||
onChange={(value) => onUpdate(rule.id, { create: value })}
|
onChange={(value) => onUpdate(rule.id, { create: value })}
|
||||||
|
|
@ -131,7 +131,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.permissionItem}>
|
<div className={styles.permissionItem}>
|
||||||
<span className={styles.permissionLabel}>Update</span>
|
<span className={styles.permissionLabel}>{t('Bearbeiten')}</span>
|
||||||
<AccessLevelSelect
|
<AccessLevelSelect
|
||||||
value={rule.update}
|
value={rule.update}
|
||||||
onChange={(value) => onUpdate(rule.id, { update: value })}
|
onChange={(value) => onUpdate(rule.id, { update: value })}
|
||||||
|
|
@ -140,7 +140,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.permissionItem}>
|
<div className={styles.permissionItem}>
|
||||||
<span className={styles.permissionLabel}>{t('delete')}</span>
|
<span className={styles.permissionLabel}>{t('Löschen')}</span>
|
||||||
<AccessLevelSelect
|
<AccessLevelSelect
|
||||||
value={rule.delete}
|
value={rule.delete}
|
||||||
onChange={(value) => onUpdate(rule.id, { delete: value })}
|
onChange={(value) => onUpdate(rule.id, { delete: value })}
|
||||||
|
|
@ -214,7 +214,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLabel = (obj: CatalogObject): string => {
|
const getLabel = (obj: CatalogObject): string => {
|
||||||
return obj.label.de || obj.label.en || obj.objectKey;
|
return (typeof obj.label === 'string' ? obj.label : '') || obj.objectKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -260,7 +260,9 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className={styles.formHint}>
|
<span className={styles.formHint}>
|
||||||
Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*).
|
{t(
|
||||||
|
'Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*).'
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -272,7 +274,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
||||||
onChange={(e) => setView(e.target.checked)}
|
onChange={(e) => setView(e.target.checked)}
|
||||||
style={{ marginRight: '0.5rem' }}
|
style={{ marginRight: '0.5rem' }}
|
||||||
/>
|
/>
|
||||||
Sichtbar (View)
|
{t('Sichtbar (Ansicht)')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -290,7 +292,12 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
||||||
{(['create', 'read', 'update', 'delete'] as const).map(op => {
|
{(['create', 'read', 'update', 'delete'] as const).map(op => {
|
||||||
const value = op === 'delete' ? del : op === 'create' ? create : op === 'update' ? update : read;
|
const value = op === 'delete' ? del : op === 'create' ? create : op === 'update' ? update : read;
|
||||||
const setValue = op === 'delete' ? setDel : op === 'create' ? setCreate : op === 'update' ? setUpdate : setRead;
|
const setValue = op === 'delete' ? setDel : op === 'create' ? setCreate : op === 'update' ? setUpdate : setRead;
|
||||||
const labels = { create: 'Create', read: 'Read', update: 'Update', delete: 'Delete' };
|
const labels = {
|
||||||
|
create: t('Erstellen'),
|
||||||
|
read: t('Lesen'),
|
||||||
|
update: t('Bearbeiten'),
|
||||||
|
delete: t('Löschen'),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={op} className={styles.matrixRow}>
|
<div key={op} className={styles.matrixRow}>
|
||||||
|
|
@ -310,7 +317,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
||||||
setValue(hierarchy[idx - 1] || 'n');
|
setValue(hierarchy[idx - 1] || 'n');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={`${labels[op]} - ${level === 'm' ? 'Eigene' : level === 'g' ? 'Gruppe' : 'Alle'}`}
|
title={`${labels[op]} - ${level === 'm' ? t('Eigene') : level === 'g' ? t('Gruppe') : t('Alle')}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -322,10 +329,10 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
||||||
|
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
<button type="button" className={styles.secondaryButton} onClick={onCancel}>
|
<button type="button" className={styles.secondaryButton} onClick={onCancel}>
|
||||||
Abbrechen
|
{t('Abbrechen')}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className={styles.primaryButton}>
|
<button type="submit" className={styles.primaryButton}>
|
||||||
<FaPlus /> Hinzufügen
|
<FaPlus /> {t('Hinzufügen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -355,6 +362,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
||||||
onDelete,
|
onDelete,
|
||||||
onAdd,
|
onAdd,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA
|
const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA
|
||||||
|
|
||||||
|
|
@ -373,9 +381,12 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
||||||
|
|
||||||
const getEmptyText = () => {
|
const getEmptyText = () => {
|
||||||
switch (context) {
|
switch (context) {
|
||||||
case 'DATA': return 'Keine Daten-Regeln definiert';
|
case 'DATA':
|
||||||
case 'UI': return 'Keine UI-Regeln definiert';
|
return t('Keine Daten-Regeln definiert');
|
||||||
case 'RESOURCE': return 'Keine Ressourcen-Regeln definiert';
|
case 'UI':
|
||||||
|
return t('Keine UI-Regeln definiert');
|
||||||
|
case 'RESOURCE':
|
||||||
|
return t('Keine Ressourcen-Regeln definiert');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -384,7 +395,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
||||||
{!readOnly && !showAddForm && (
|
{!readOnly && !showAddForm && (
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<span className={styles.sectionTitle}>
|
<span className={styles.sectionTitle}>
|
||||||
{rules.length} {rules.length === 1 ? 'Regel' : 'Regeln'}
|
{rules.length} {rules.length === 1 ? t('Regel') : t('Regeln')}
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
{/* View Toggle */}
|
{/* View Toggle */}
|
||||||
|
|
@ -393,14 +404,14 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
||||||
<button
|
<button
|
||||||
className={`${styles.viewToggleButton} ${useTableView ? styles.active : ''}`}
|
className={`${styles.viewToggleButton} ${useTableView ? styles.active : ''}`}
|
||||||
onClick={() => setUseTableView(true)}
|
onClick={() => setUseTableView(true)}
|
||||||
title="Tabellenansicht"
|
title={t('Tabellenansicht')}
|
||||||
>
|
>
|
||||||
<FaThList />
|
<FaThList />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${styles.viewToggleButton} ${!useTableView ? styles.active : ''}`}
|
className={`${styles.viewToggleButton} ${!useTableView ? styles.active : ''}`}
|
||||||
onClick={() => setUseTableView(false)}
|
onClick={() => setUseTableView(false)}
|
||||||
title="Kartenansicht"
|
title={t('Kartenansicht')}
|
||||||
>
|
>
|
||||||
<FaTh />
|
<FaTh />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -410,7 +421,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
||||||
className={styles.addButton}
|
className={styles.addButton}
|
||||||
onClick={() => setShowAddForm(true)}
|
onClick={() => setShowAddForm(true)}
|
||||||
>
|
>
|
||||||
<FaPlus /> Neue Regel
|
<FaPlus /> {t('Neue Regel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -431,7 +442,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
||||||
<p className={styles.emptyText}>{getEmptyText()}</p>
|
<p className={styles.emptyText}>{getEmptyText()}</p>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<p className={styles.emptyHint}>
|
<p className={styles.emptyHint}>
|
||||||
Klicken Sie auf "Neue Regel" um eine Berechtigung hinzuzufügen.
|
{t('Klicken Sie auf „Neue Regel“, um eine Berechtigung hinzuzufügen.')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -469,6 +480,7 @@ interface JsonEditorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) => {
|
const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [jsonText, setJsonText] = useState('');
|
const [jsonText, setJsonText] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -481,7 +493,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(jsonText);
|
const parsed = JSON.parse(jsonText);
|
||||||
if (!Array.isArray(parsed)) {
|
if (!Array.isArray(parsed)) {
|
||||||
throw new Error('JSON muss ein Array sein');
|
throw new Error(t('JSON muss ein Array sein'));
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
onApply(parsed);
|
onApply(parsed);
|
||||||
|
|
@ -501,8 +513,9 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
|
||||||
/>
|
/>
|
||||||
{error && <div className={styles.jsonError}>{error}</div>}
|
{error && <div className={styles.jsonError}>{error}</div>}
|
||||||
<p className={styles.jsonHint}>
|
<p className={styles.jsonHint}>
|
||||||
Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON.
|
{t(
|
||||||
Änderungen werden erst nach Klick auf "Anwenden" übernommen.
|
'Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON. Änderungen werden erst nach Klick auf „Anwenden“ übernommen.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
|
|
@ -512,7 +525,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
|
||||||
onClick={handleApply}
|
onClick={handleApply}
|
||||||
disabled={!!error}
|
disabled={!!error}
|
||||||
>
|
>
|
||||||
JSON anwenden
|
{t('JSON anwenden')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -607,7 +620,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
onSave?.();
|
onSave?.();
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Speichern');
|
showError(t('Fehler'), result.error || t('Fehler beim Speichern'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -655,7 +668,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
||||||
<div className={styles.editorHeader}>
|
<div className={styles.editorHeader}>
|
||||||
<h3 className={styles.editorTitle}>
|
<h3 className={styles.editorTitle}>
|
||||||
Berechtigungen{roleName ? `: ${roleName}` : ''}
|
Berechtigungen{roleName ? `: ${roleName}` : ''}
|
||||||
{isTemplate && <span className={styles.templateBadge}>Template</span>}
|
{isTemplate && <span className={styles.templateBadge}>{t('Vorlage')}</span>}
|
||||||
</h3>
|
</h3>
|
||||||
{!readOnly && hasChanges && (
|
{!readOnly && hasChanges && (
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const opTitle = (op: 'create' | 'read' | 'update' | 'delete') =>
|
||||||
|
({ create: t('Erstellen'), read: t('Lesen'), update: t('Bearbeiten'), delete: t('Löschen') })[op];
|
||||||
const handleLevelToggle = (
|
const handleLevelToggle = (
|
||||||
field: 'read' | 'create' | 'update' | 'delete',
|
field: 'read' | 'create' | 'update' | 'delete',
|
||||||
targetLevel: 'm' | 'g' | 'a',
|
targetLevel: 'm' | 'g' | 'a',
|
||||||
|
|
@ -112,7 +114,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
||||||
checked={rule.view}
|
checked={rule.view}
|
||||||
onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
|
onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
title="Sichtbar"
|
title={t('Sichtbar')}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
@ -127,7 +129,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
||||||
checked={hasLevel(rule[op] as AccessLevel, 'm')}
|
checked={hasLevel(rule[op] as AccessLevel, 'm')}
|
||||||
onChange={(e) => handleLevelToggle(op, 'm', e.target.checked)}
|
onChange={(e) => handleLevelToggle(op, 'm', e.target.checked)}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Eigene`}
|
title={`${opTitle(op)} - ${t('Eigene')}`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
@ -140,7 +142,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
||||||
checked={hasLevel(rule[op] as AccessLevel, 'g')}
|
checked={hasLevel(rule[op] as AccessLevel, 'g')}
|
||||||
onChange={(e) => handleLevelToggle(op, 'g', e.target.checked)}
|
onChange={(e) => handleLevelToggle(op, 'g', e.target.checked)}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Gruppe`}
|
title={`${opTitle(op)} - ${t('Gruppe')}`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
@ -153,7 +155,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
||||||
checked={hasLevel(rule[op] as AccessLevel, 'a')}
|
checked={hasLevel(rule[op] as AccessLevel, 'a')}
|
||||||
onChange={(e) => handleLevelToggle(op, 'a', e.target.checked)}
|
onChange={(e) => handleLevelToggle(op, 'a', e.target.checked)}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Alle`}
|
title={`${opTitle(op)} - ${t('Alle')}`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
@ -166,7 +168,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
||||||
<button
|
<button
|
||||||
className={`${styles.iconButton} ${styles.danger}`}
|
className={`${styles.iconButton} ${styles.danger}`}
|
||||||
onClick={() => onDelete(rule.id)}
|
onClick={() => onDelete(rule.id)}
|
||||||
title={t('delete rule')}
|
title={t('Regel löschen')}
|
||||||
>
|
>
|
||||||
<FaTrash />
|
<FaTrash />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -200,7 +202,7 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={styles.colObject}>{t('object dot notation')}</th>
|
<th className={styles.colObject}>{t('object dot notation')}</th>
|
||||||
<th className={styles.colView}>View</th>
|
<th className={styles.colView}>{t('Ansicht')}</th>
|
||||||
{isDataContext && (
|
{isDataContext && (
|
||||||
<>
|
<>
|
||||||
<th className={styles.colGroupHeader} colSpan={4}>{t('own')}</th>
|
<th className={styles.colGroupHeader} colSpan={4}>{t('own')}</th>
|
||||||
|
|
@ -214,18 +216,18 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
||||||
<tr className={styles.subHeader}>
|
<tr className={styles.subHeader}>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th title="Create">C</th>
|
<th title={t('Erstellen')}>C</th>
|
||||||
<th title="Read">R</th>
|
<th title={t('Lesen')}>R</th>
|
||||||
<th title="Update">U</th>
|
<th title={t('Bearbeiten')}>U</th>
|
||||||
<th title={t('delete')}>D</th>
|
<th title={t('Löschen')}>D</th>
|
||||||
<th title="Create">C</th>
|
<th title={t('Erstellen')}>C</th>
|
||||||
<th title="Read">R</th>
|
<th title={t('Lesen')}>R</th>
|
||||||
<th title="Update">U</th>
|
<th title={t('Bearbeiten')}>U</th>
|
||||||
<th title={t('delete')}>D</th>
|
<th title={t('Löschen')}>D</th>
|
||||||
<th title="Create">C</th>
|
<th title={t('Erstellen')}>C</th>
|
||||||
<th title="Read">R</th>
|
<th title={t('Lesen')}>R</th>
|
||||||
<th title="Update">U</th>
|
<th title={t('Bearbeiten')}>U</th>
|
||||||
<th title={t('delete')}>D</th>
|
<th title={t('Löschen')}>D</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
* Simple text input with send button, usable by both Workspace and Editor.
|
* Simple text input with send button, usable by both Workspace and Editor.
|
||||||
*/
|
*/
|
||||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
onSend: (message: string) => void;
|
onSend: (message: string) => void;
|
||||||
|
|
@ -17,11 +18,13 @@ interface ChatInputProps {
|
||||||
export const ChatInput: React.FC<ChatInputProps> = ({
|
export const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
onSend,
|
onSend,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
placeholder = 'Type a message...',
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const resolvedPlaceholder = placeholder ?? t('Nachricht eingeben…');
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
|
@ -62,7 +65,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
onKeyDown={_handleKeyDown}
|
onKeyDown={_handleKeyDown}
|
||||||
placeholder={placeholder}
|
placeholder={resolvedPlaceholder}
|
||||||
disabled={isProcessing || disabled}
|
disabled={isProcessing || disabled}
|
||||||
rows={1}
|
rows={1}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -95,7 +98,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isProcessing ? '...' : 'Send'}
|
{isProcessing ? '…' : t('Senden')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -31,9 +32,11 @@ const _roleColors: Record<string, string> = {
|
||||||
export const ChatMessageList: React.FC<ChatMessageListProps> = ({
|
export const ChatMessageList: React.FC<ChatMessageListProps> = ({
|
||||||
messages,
|
messages,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
emptyMessage = 'No messages yet.',
|
emptyMessage,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const resolvedEmpty = emptyMessage ?? t('Noch keine Nachrichten.');
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -55,7 +58,7 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({
|
||||||
>
|
>
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px', textAlign: 'center', marginTop: '24px' }}>
|
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px', textAlign: 'center', marginTop: '24px' }}>
|
||||||
{emptyMessage}
|
{resolvedEmpty}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
|
|
@ -80,7 +83,7 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({
|
||||||
))}
|
))}
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '12px', fontStyle: 'italic' }}>
|
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '12px', fontStyle: 'italic' }}>
|
||||||
Processing...
|
{t('Wird verarbeitet…')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export function ContentPreview({
|
||||||
setError(t('Ungültige Datei-ID'));
|
setError(t('Ungültige Datei-ID'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!fileName || fileName === 'Unknown Item') {
|
if (!fileName || fileName === 'Unknown Item' || fileName === 'Unbekanntes Element') {
|
||||||
setError(t('Dateiname nicht verfügbar'));
|
setError(t('Dateiname nicht verfügbar'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ export function ContentPreview({
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, [isOpen, fileId, fileName]);
|
}, [isOpen, fileId, fileName, t]);
|
||||||
|
|
||||||
|
|
||||||
const loadPreview = async () => {
|
const loadPreview = async () => {
|
||||||
|
|
@ -95,7 +95,7 @@ export function ContentPreview({
|
||||||
}
|
}
|
||||||
// If it's text content but MIME type says PDF, we'll handle it in renderPreview
|
// If it's text content but MIME type says PDF, we'll handle it in renderPreview
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Failed to load preview');
|
setError(result.error || t('Vorschau konnte nicht geladen werden.'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(t('Ein unerwarteter Fehler ist aufgetreten, während'));
|
setError(t('Ein unerwarteter Fehler ist aufgetreten, während'));
|
||||||
|
|
@ -201,7 +201,7 @@ export function ContentPreview({
|
||||||
</div>
|
</div>
|
||||||
<pre className={styles.jsonPreview}>
|
<pre className={styles.jsonPreview}>
|
||||||
<code className={styles.jsonCode}>
|
<code className={styles.jsonCode}>
|
||||||
{previewContent || 'No content available'}
|
{previewContent || t('Kein Inhalt verfügbar')}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,9 @@ export function UrlContentPreview({
|
||||||
|
|
||||||
const warningTimeout = setTimeout(() => {
|
const warningTimeout = setTimeout(() => {
|
||||||
if (isLoading && !hasLoaded) {
|
if (isLoading && !hasLoaded) {
|
||||||
setWarning('PDF lädt langsam. Sie können es auch direkt herunterladen oder in einem neuen Tab öffnen.');
|
setWarning(
|
||||||
|
t('PDF lädt langsam. Sie können es auch direkt herunterladen oder in einem neuen Tab öffnen.')
|
||||||
|
);
|
||||||
// Don't set isLoading to false - let it continue
|
// Don't set isLoading to false - let it continue
|
||||||
}
|
}
|
||||||
}, WARNING_TIMEOUT);
|
}, WARNING_TIMEOUT);
|
||||||
|
|
@ -107,7 +109,7 @@ export function UrlContentPreview({
|
||||||
console.log('PDF loading timeout, switching to PDF.js fallback');
|
console.log('PDF loading timeout, switching to PDF.js fallback');
|
||||||
setUsePdfJs(true);
|
setUsePdfJs(true);
|
||||||
setIsLoading(true); // Restart loading with PDF.js
|
setIsLoading(true); // Restart loading with PDF.js
|
||||||
setWarning('PDF lädt langsam. Versuche alternative Anzeigemethode...');
|
setWarning(t('PDF lädt langsam. Alternative Anzeigemethode wird versucht…'));
|
||||||
} else if (isLoading && !hasLoaded && usePdfJs) {
|
} else if (isLoading && !hasLoaded && usePdfJs) {
|
||||||
// PDF.js also failed, show error
|
// PDF.js also failed, show error
|
||||||
setShowPdfAnyway(true);
|
setShowPdfAnyway(true);
|
||||||
|
|
@ -121,7 +123,7 @@ export function UrlContentPreview({
|
||||||
clearTimeout(errorTimeout);
|
clearTimeout(errorTimeout);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [isOpen, isLoading, hasLoaded, usePdfJs]);
|
}, [isOpen, isLoading, hasLoaded, usePdfJs, t]);
|
||||||
|
|
||||||
// Validate URL
|
// Validate URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -184,7 +186,7 @@ export function UrlContentPreview({
|
||||||
padding: '0.5rem 1rem'
|
padding: '0.5rem 1rem'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
In neuem Tab öffnen
|
{t('In neuem Tab öffnen')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
|
|
@ -195,7 +197,7 @@ export function UrlContentPreview({
|
||||||
padding: '0.5rem 1rem'
|
padding: '0.5rem 1rem'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download
|
{t('Herunterladen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -241,7 +243,7 @@ export function UrlContentPreview({
|
||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
In neuem Tab öffnen
|
{t('In neuem Tab öffnen')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
|
|
@ -253,7 +255,7 @@ export function UrlContentPreview({
|
||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download File
|
{t('Datei herunterladen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -284,7 +286,7 @@ export function UrlContentPreview({
|
||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
In neuem Tab öffnen
|
{t('In neuem Tab öffnen')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
|
|
@ -296,7 +298,7 @@ export function UrlContentPreview({
|
||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download
|
{t('Herunterladen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -316,7 +318,7 @@ export function UrlContentPreview({
|
||||||
<div className={styles.fileName}>{fileName}</div>
|
<div className={styles.fileName}>{fileName}</div>
|
||||||
<p>{t('Vorschau wird hierfür nicht unterstützt')}</p>
|
<p>{t('Vorschau wird hierfür nicht unterstützt')}</p>
|
||||||
<button onClick={handleDownload} className={styles.retryButton}>
|
<button onClick={handleDownload} className={styles.retryButton}>
|
||||||
Download File
|
{t('Datei herunterladen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
||||||
<button
|
<button
|
||||||
className={styles.collapseButton}
|
className={styles.collapseButton}
|
||||||
onClick={() => toggleCollapse(rowPath)}
|
onClick={() => toggleCollapse(rowPath)}
|
||||||
title={isCollapsed ? 'Expand' : 'Collapse'}
|
title={isCollapsed ? t('Aufklappen') : t('Einklappen')}
|
||||||
>
|
>
|
||||||
{isCollapsed ? '▶' : '▼'}
|
{isCollapsed ? '▶' : '▼'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -479,7 +479,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
||||||
);
|
);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
const rawData = {
|
const rawData = {
|
||||||
keys: ['Raw Content'],
|
keys: [t('Rohinhalt')],
|
||||||
values: [previewContent],
|
values: [previewContent],
|
||||||
types: ['string'],
|
types: ['string'],
|
||||||
isNested: [false]
|
isNested: [false]
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export function PdfJsRenderer({
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading PDF with PDF.js:', err);
|
console.error('Error loading PDF with PDF.js:', err);
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load PDF');
|
setError(err instanceof Error ? err.message : t('PDF konnte nicht geladen werden.'));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
onError();
|
onError();
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +116,7 @@ export function PdfJsRenderer({
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error rendering PDF page:', err);
|
console.error('Error rendering PDF page:', err);
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to render PDF page');
|
setError(err instanceof Error ? err.message : t('PDF-Seite konnte nicht gerendert werden.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -132,7 +132,9 @@ export function PdfJsRenderer({
|
||||||
return (
|
return (
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<div className={styles.errorIcon}>⚠️</div>
|
<div className={styles.errorIcon}>⚠️</div>
|
||||||
<p>Fehler beim Laden der PDF: {error}</p>
|
<p>
|
||||||
|
{t('Fehler beim Laden der PDF:')} {error}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,8 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
const LOG = '[Automation2]';
|
const LOG = '[Automation2]';
|
||||||
|
|
||||||
const DEFAULT_INVOCATIONS = (): WorkflowEntryPoint[] =>
|
const _buildDefaultInvocations = (runLabel: string): WorkflowEntryPoint[] =>
|
||||||
buildInvocationsForPrimaryKind('manual', [], 'Jetzt ausführen');
|
buildInvocationsForPrimaryKind('manual', [], runLabel);
|
||||||
|
|
||||||
interface Automation2FlowEditorProps {
|
interface Automation2FlowEditorProps {
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
|
|
@ -106,7 +106,9 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||||
const [selectedNode, setSelectedNode] = useState<CanvasNode | null>(null);
|
const [selectedNode, setSelectedNode] = useState<CanvasNode | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [invocations, setInvocations] = useState<WorkflowEntryPoint[]>(DEFAULT_INVOCATIONS);
|
const [invocations, setInvocations] = useState<WorkflowEntryPoint[]>(() =>
|
||||||
|
_buildDefaultInvocations(t('Jetzt ausführen'))
|
||||||
|
);
|
||||||
const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
|
const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
|
||||||
const [leftPanelOpen, setLeftPanelOpen] = useState(true);
|
const [leftPanelOpen, setLeftPanelOpen] = useState(true);
|
||||||
const [tracingRunId, setTracingRunId] = useState<string | null>(null);
|
const [tracingRunId, setTracingRunId] = useState<string | null>(null);
|
||||||
|
|
@ -176,7 +178,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
|
|
||||||
const applyGraphWithSync = useCallback(
|
const applyGraphWithSync = useCallback(
|
||||||
(graph: Automation2Graph | null | undefined, wfInvocations: WorkflowEntryPoint[] | undefined) => {
|
(graph: Automation2Graph | null | undefined, wfInvocations: WorkflowEntryPoint[] | undefined) => {
|
||||||
const inv = wfInvocations?.length ? wfInvocations : DEFAULT_INVOCATIONS();
|
const inv = wfInvocations?.length ? wfInvocations : _buildDefaultInvocations(t('Jetzt ausführen'));
|
||||||
setInvocations(inv);
|
setInvocations(inv);
|
||||||
if (!graph?.nodes?.length) {
|
if (!graph?.nodes?.length) {
|
||||||
const synced = syncCanvasStartNode([], [], inv, nodeTypes, language);
|
const synced = syncCanvasStartNode([], [], inv, nodeTypes, language);
|
||||||
|
|
@ -189,7 +191,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
setCanvasNodes(synced.nodes);
|
setCanvasNodes(synced.nodes);
|
||||||
setCanvasConnections(synced.connections);
|
setCanvasConnections(synced.connections);
|
||||||
},
|
},
|
||||||
[nodeTypes, language]
|
[nodeTypes, language, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFromApiGraph = useCallback(
|
const handleFromApiGraph = useCallback(
|
||||||
|
|
@ -202,7 +204,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
const handleExecute = useCallback(async () => {
|
const handleExecute = useCallback(async () => {
|
||||||
const graph = toApiGraph(canvasNodes, canvasConnections);
|
const graph = toApiGraph(canvasNodes, canvasConnections);
|
||||||
if (graph.nodes.length === 0) {
|
if (graph.nodes.length === 0) {
|
||||||
setExecuteResult({ success: false, error: 'Keine Nodes im Workflow.' });
|
setExecuteResult({ success: false, error: t('Keine Nodes im Workflow.') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setExecuting(true);
|
setExecuting(true);
|
||||||
|
|
@ -222,12 +224,12 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
} finally {
|
} finally {
|
||||||
setExecuting(false);
|
setExecuting(false);
|
||||||
}
|
}
|
||||||
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations]);
|
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations, t]);
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
const graph = toApiGraph(canvasNodes, canvasConnections);
|
const graph = toApiGraph(canvasNodes, canvasConnections);
|
||||||
if (graph.nodes.length === 0) {
|
if (graph.nodes.length === 0) {
|
||||||
setExecuteResult({ success: false, error: 'Keine Nodes zum Speichern.' });
|
setExecuteResult({ success: false, error: t('Keine Nodes zum Speichern.') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
@ -236,17 +238,17 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
await updateWorkflow(request, instanceId, currentWorkflowId, { graph, invocations });
|
await updateWorkflow(request, instanceId, currentWorkflowId, { graph, invocations });
|
||||||
setExecuteResult({ success: true } as ExecuteGraphResponse);
|
setExecuteResult({ success: true } as ExecuteGraphResponse);
|
||||||
} else {
|
} else {
|
||||||
const label = await promptInput('Workflow-Name:', {
|
const label = await promptInput(t('Workflow-Name:'), {
|
||||||
title: t('Workflow speichern'),
|
title: t('Workflow speichern'),
|
||||||
defaultValue: 'Neuer Workflow',
|
defaultValue: t('Neuer Workflow'),
|
||||||
placeholder: 'Name des Workflows',
|
placeholder: t('Name des Workflows'),
|
||||||
});
|
});
|
||||||
if (!label) {
|
if (!label) {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const created = await createWorkflow(request, instanceId, {
|
const created = await createWorkflow(request, instanceId, {
|
||||||
label: label.trim() || 'Neuer Workflow',
|
label: label.trim() || t('Neuer Workflow'),
|
||||||
graph,
|
graph,
|
||||||
invocations,
|
invocations,
|
||||||
});
|
});
|
||||||
|
|
@ -260,7 +262,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations]);
|
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t]);
|
||||||
|
|
||||||
const handleLoad = useCallback(
|
const handleLoad = useCallback(
|
||||||
async (workflowId: string) => {
|
async (workflowId: string) => {
|
||||||
|
|
@ -287,17 +289,17 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
if (workflowId) handleLoad(workflowId);
|
if (workflowId) handleLoad(workflowId);
|
||||||
else {
|
else {
|
||||||
setExecuteResult(null);
|
setExecuteResult(null);
|
||||||
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS());
|
applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleLoad, applyGraphWithSync]
|
[handleLoad, applyGraphWithSync, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNew = useCallback(() => {
|
const handleNew = useCallback(() => {
|
||||||
setCurrentWorkflowId(null);
|
setCurrentWorkflowId(null);
|
||||||
setExecuteResult(null);
|
setExecuteResult(null);
|
||||||
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS());
|
applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
|
||||||
}, [applyGraphWithSync]);
|
}, [applyGraphWithSync, t]);
|
||||||
|
|
||||||
const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record<string, unknown>) => {
|
const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record<string, unknown>) => {
|
||||||
setCanvasNodes((prev) =>
|
setCanvasNodes((prev) =>
|
||||||
|
|
@ -401,7 +403,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
if (loading || nodeTypes.length === 0) return;
|
if (loading || nodeTypes.length === 0) return;
|
||||||
if (currentWorkflowId || initialWorkflowId) return;
|
if (currentWorkflowId || initialWorkflowId) return;
|
||||||
if (canvasNodes.length > 0) return;
|
if (canvasNodes.length > 0) return;
|
||||||
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS());
|
applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
|
||||||
}, [
|
}, [
|
||||||
loading,
|
loading,
|
||||||
nodeTypes.length,
|
nodeTypes.length,
|
||||||
|
|
@ -409,6 +411,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
initialWorkflowId,
|
initialWorkflowId,
|
||||||
canvasNodes.length,
|
canvasNodes.length,
|
||||||
applyGraphWithSync,
|
applyGraphWithSync,
|
||||||
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const toggleCategory = useCallback((id: string) => {
|
const toggleCategory = useCallback((id: string) => {
|
||||||
|
|
@ -591,7 +594,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
return (
|
return (
|
||||||
<div className={styles.sidebar} style={_sidebarStyle}>
|
<div className={styles.sidebar} style={_sidebarStyle}>
|
||||||
<div className={styles.sidebarHeader}>
|
<div className={styles.sidebarHeader}>
|
||||||
<h3 className={styles.sidebarTitle}>Nodes</h3>
|
<h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.loading}>
|
<div className={styles.loading}>
|
||||||
<FaSpinner className={styles.spinner} style={{ marginBottom: '0.5rem' }} />
|
<FaSpinner className={styles.spinner} style={{ marginBottom: '0.5rem' }} />
|
||||||
|
|
@ -604,12 +607,12 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
return (
|
return (
|
||||||
<div className={styles.sidebar} style={_sidebarStyle}>
|
<div className={styles.sidebar} style={_sidebarStyle}>
|
||||||
<div className={styles.sidebarHeader}>
|
<div className={styles.sidebarHeader}>
|
||||||
<h3 className={styles.sidebarTitle}>Nodes</h3>
|
<h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
<button className={styles.retryButton} onClick={loadNodeTypes}>
|
<button className={styles.retryButton} onClick={loadNodeTypes}>
|
||||||
Erneut versuchen
|
{t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -648,7 +651,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
className={`${styles.rightTab} ${udbTab === tab ? styles.rightTabActive : ''}`}
|
className={`${styles.rightTab} ${udbTab === tab ? styles.rightTabActive : ''}`}
|
||||||
onClick={() => setUdbTab(tab)}
|
onClick={() => setUdbTab(tab)}
|
||||||
>
|
>
|
||||||
{{ chats: 'Chats', files: 'Dateien', sources: 'Quellen' }[tab]}
|
{{ chats: t('Chats'), files: t('Dateien'), sources: t('Quellen') }[tab]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -755,13 +758,13 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}
|
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}
|
||||||
onClick={() => setRightTab('nodes')}
|
onClick={() => setRightTab('nodes')}
|
||||||
>
|
>
|
||||||
Nodes
|
{t('Knoten')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${styles.rightTab} ${rightTab === 'tracing' ? styles.rightTabActive : ''}`}
|
className={`${styles.rightTab} ${rightTab === 'tracing' ? styles.rightTabActive : ''}`}
|
||||||
onClick={() => { setRightTab('tracing'); if (!tracingRunId) setTracingRunId('select'); }}
|
onClick={() => { setRightTab('tracing'); if (!tracingRunId) setTracingRunId('select'); }}
|
||||||
>
|
>
|
||||||
Tracing
|
{t('Ablaufverfolgung')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen), version selector, and execute result.
|
* CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen), version selector, and execute result.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown } from 'react-icons/fa';
|
import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown } from 'react-icons/fa';
|
||||||
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
|
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
|
||||||
import styles from './Automation2FlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
@ -118,7 +118,15 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
return () => document.removeEventListener('mousedown', _handleClickOutside);
|
return () => document.removeEventListener('mousedown', _handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const SCOPE_LABELS: Record<string, string> = { user: 'Meine Vorlagen', instance: 'Instanz', mandate: 'Mandant' };
|
const scopeLabels = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
user: t('Meine Vorlagen'),
|
||||||
|
instance: t('Instanz'),
|
||||||
|
mandate: t('Mandant'),
|
||||||
|
}) as Record<string, string>,
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.canvasHeader}>
|
<div className={styles.canvasHeader}>
|
||||||
|
|
@ -139,14 +147,14 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
className={styles.canvasTitle}
|
className={styles.canvasTitle}
|
||||||
style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }}
|
style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }}
|
||||||
onClick={_startNameEdit}
|
onClick={_startNameEdit}
|
||||||
title={onWorkflowRename ? 'Klicken zum Umbenennen' : undefined}
|
title={onWorkflowRename ? t('Klicken zum Umbenennen') : undefined}
|
||||||
>
|
>
|
||||||
{currentWorkflow.label}
|
{currentWorkflow.label}
|
||||||
</h4>
|
</h4>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<h4 className={styles.canvasTitle} style={{ margin: 0, fontStyle: 'italic', opacity: 0.6 }}>
|
<h4 className={styles.canvasTitle} style={{ margin: 0, fontStyle: 'italic', opacity: 0.6 }}>
|
||||||
Neuer Workflow
|
{t('Neuer Workflow')}
|
||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
{onWorkflowSettings && (
|
{onWorkflowSettings && (
|
||||||
|
|
@ -154,7 +162,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.canvasGearBtn}
|
className={styles.canvasGearBtn}
|
||||||
title={t('Workflowkonfiguration Einstieg/Starts')}
|
title={t('Workflowkonfiguration Einstieg/Starts')}
|
||||||
aria-label="Workflow-Konfiguration"
|
aria-label={t('Workflow-Konfiguration')}
|
||||||
onClick={onWorkflowSettings}
|
onClick={onWorkflowSettings}
|
||||||
>
|
>
|
||||||
<FaCog />
|
<FaCog />
|
||||||
|
|
@ -165,7 +173,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
<div ref={newMenuRef} style={{ position: 'relative', display: 'inline-block' }}>
|
<div ref={newMenuRef} style={{ position: 'relative', display: 'inline-block' }}>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<button type="button" className={styles.retryButton} onClick={onNew} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}>
|
<button type="button" className={styles.retryButton} onClick={onNew} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}>
|
||||||
Neu
|
{t('Neu')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -184,7 +192,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
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' }}
|
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem' }}
|
||||||
>
|
>
|
||||||
Leerer Workflow
|
{t('Leerer Workflow')}
|
||||||
</button>
|
</button>
|
||||||
{onNewFromTemplate && (
|
{onNewFromTemplate && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -192,7 +200,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
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)' }}
|
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)' }}
|
||||||
>
|
>
|
||||||
Aus Vorlage...
|
{t('Aus Vorlage…')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -205,7 +213,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={saving || !hasNodes}
|
disabled={saving || !hasNodes}
|
||||||
>
|
>
|
||||||
{saving ? <FaSpinner className={styles.spinner} /> : 'Speichern'}
|
{saving ? <FaSpinner className={styles.spinner} /> : t('Speichern')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Save as template */}
|
{/* Save as template */}
|
||||||
|
|
@ -229,7 +237,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
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 }}
|
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 }}
|
||||||
>
|
>
|
||||||
{SCOPE_LABELS[s]}
|
{scopeLabels[s]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -260,19 +268,19 @@ 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={{ marginRight: '0.5rem', display: 'inline-block' }} />
|
||||||
Ausführen…
|
{t('Ausführen…')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaPlay style={{ marginRight: '0.5rem' }} />
|
<FaPlay style={{ marginRight: '0.5rem' }} />
|
||||||
Ausführen
|
{t('Ausführen')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{onToggleChat && (
|
{onToggleChat && (
|
||||||
<button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('Workspace-Panel: Chats, Dateien, Quellen')}>
|
<button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('Workspace-Panel: Chats, Dateien, Quellen')}>
|
||||||
<FaDatabase style={{ marginRight: '0.4rem' }} />
|
<FaDatabase style={{ marginRight: '0.4rem' }} />
|
||||||
Workspace
|
{t('Workspace')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -280,7 +288,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
{/* Version Selector */}
|
{/* 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 style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.5rem', flexWrap: 'wrap' }}>
|
||||||
<span style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary, #666)' }}>Version:</span>
|
<span style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary, #666)' }}>{t('Version:')}</span>
|
||||||
<select
|
<select
|
||||||
value={currentVersionId ?? ''}
|
value={currentVersionId ?? ''}
|
||||||
onChange={(e) => onVersionSelect?.(e.target.value || null)}
|
onChange={(e) => onVersionSelect?.(e.target.value || null)}
|
||||||
|
|
@ -316,7 +324,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||||
>
|
>
|
||||||
<FaCloudUploadAlt style={{ marginRight: 4 }} />
|
<FaCloudUploadAlt style={{ marginRight: 4 }} />
|
||||||
Publish
|
{t('Veröffentlichen')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{currentVersion && currentStatus === 'published' && onUnpublishVersion && (
|
{currentVersion && currentStatus === 'published' && onUnpublishVersion && (
|
||||||
|
|
@ -329,7 +337,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||||
>
|
>
|
||||||
<FaCloudDownloadAlt style={{ marginRight: 4 }} />
|
<FaCloudDownloadAlt style={{ marginRight: 4 }} />
|
||||||
Unpublish
|
{t('Veröffentlichung aufheben')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{currentVersion && currentStatus !== 'archived' && onArchiveVersion && (
|
{currentVersion && currentStatus !== 'archived' && onArchiveVersion && (
|
||||||
|
|
@ -388,7 +396,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||||
Task zu bearbeiten.
|
Task zu bearbeiten.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>✗ {executeResult.error ?? 'Unbekannter Fehler'}</>
|
<>✗ {executeResult.error ?? t('Unbekannter Fehler')}</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -130,29 +130,29 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
if (!accumulated) {
|
if (!accumulated) {
|
||||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: 'Done.' } : m));
|
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: t('Fertig.') } : m));
|
||||||
}
|
}
|
||||||
onGraphUpdated?.();
|
onGraphUpdated?.();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
onError: (event) => {
|
onError: (event) => {
|
||||||
const errText = event.content || 'Request failed';
|
const errText = event.content || t('Anfrage fehlgeschlagen');
|
||||||
if (!accumulated) {
|
if (!accumulated) {
|
||||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `Error: ${errText}` } : m));
|
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `${t('Fehler')}: ${errText}` } : m));
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
onStopped: () => setLoading(false),
|
onStopped: () => setLoading(false),
|
||||||
},
|
},
|
||||||
onConnectionError: (err) => {
|
onConnectionError: (err) => {
|
||||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `Error: ${err.message}` } : m));
|
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `${t('Fehler')}: ${err.message}` } : m));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
onStreamEnd: () => setLoading(false),
|
onStreamEnd: () => setLoading(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
abortRef.current = cleanup;
|
abortRef.current = cleanup;
|
||||||
}, [prompt, loading, workflowId, instanceId, messages, onGraphUpdated, pendingFiles, attachedDataSourceIds, attachedFeatureDataSourceIds]);
|
}, [prompt, loading, workflowId, instanceId, messages, onGraphUpdated, pendingFiles, attachedDataSourceIds, attachedFeatureDataSourceIds, t]);
|
||||||
|
|
||||||
const _handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const _handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
|
@ -325,7 +325,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
minWidth: 220, maxHeight: 260, overflowY: 'auto',
|
minWidth: 220, maxHeight: 260, overflowY: 'auto',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}>
|
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}>
|
||||||
Active Sources auswählen
|
{t('Aktive Quellen auswählen')}
|
||||||
</div>
|
</div>
|
||||||
{dataSources.map(ds => {
|
{dataSources.map(ds => {
|
||||||
const isSelected = attachedDataSourceIds.includes(ds.id);
|
const isSelected = attachedDataSourceIds.includes(ds.id);
|
||||||
|
|
@ -354,7 +354,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
{featureDataSources.length > 0 && (
|
{featureDataSources.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderTop: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0' }}>
|
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderTop: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0' }}>
|
||||||
Feature Data Sources
|
{t('Feature-Datenquellen')}
|
||||||
</div>
|
</div>
|
||||||
{featureDataSources.map(fds => {
|
{featureDataSources.map(fds => {
|
||||||
const isSelected = attachedFeatureDataSourceIds.includes(fds.id);
|
const isSelected = attachedFeatureDataSourceIds.includes(fds.id);
|
||||||
|
|
@ -394,7 +394,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
<button onClick={() => abortRef.current?.()} style={{
|
<button onClick={() => abortRef.current?.()} style={{
|
||||||
padding: '8px 14px', borderRadius: 8, border: 'none',
|
padding: '8px 14px', borderRadius: 8, border: 'none',
|
||||||
background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600, fontSize: 12,
|
background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600, fontSize: 12,
|
||||||
}}>Stop</button>
|
}}>{t('Stopp')}</button>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={_handleSend} disabled={!prompt.trim() || !workflowId} style={{
|
<button onClick={_handleSend} disabled={!prompt.trim() || !workflowId} style={{
|
||||||
padding: '8px 14px', borderRadius: 8, border: 'none',
|
padding: '8px 14px', borderRadius: 8, border: 'none',
|
||||||
|
|
|
||||||
|
|
@ -560,17 +560,30 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
||||||
>
|
>
|
||||||
{selectedNodeIds.size > 1 && !selectedConnectionId && !connectingFrom && (
|
{selectedNodeIds.size > 1 && !selectedConnectionId && !connectingFrom && (
|
||||||
<div className={styles.connectionHint}>
|
<div className={styles.connectionHint}>
|
||||||
{selectedNodeIds.size} Nodes ausgewählt • <kbd>Entf</kbd> zum Löschen • Ziehen zum Verschieben • <kbd>Shift</kbd>+Klick zum Hinzufügen/Entfernen
|
{selectedNodeIds.size} {t('Knoten ausgewählt')}
|
||||||
|
{' · '}
|
||||||
|
<kbd>Entf</kbd> {t('zum Löschen')}
|
||||||
|
{' · '}
|
||||||
|
{t('Ziehen zum Verschieben')}
|
||||||
|
{' · '}
|
||||||
|
<kbd>Shift</kbd>
|
||||||
|
{t('+Klick zum Hinzufügen oder Entfernen')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{connectingFrom && !selectedConnectionId && (
|
{connectingFrom && !selectedConnectionId && (
|
||||||
<div className={styles.connectionHint}>
|
<div className={styles.connectionHint}>
|
||||||
Ziehen Sie zum Eingang oder klicken Sie auf einen Eingang • <kbd>Esc</kbd> zum Abbrechen
|
{t('Ziehen Sie zum Eingang oder klicken Sie auf einen Eingang')}
|
||||||
|
{' · '}
|
||||||
|
<kbd>Esc</kbd> {t('zum Abbrechen')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedConnectionId && (
|
{selectedConnectionId && (
|
||||||
<div className={styles.connectionHint}>
|
<div className={styles.connectionHint}>
|
||||||
Pfeil ausgewählt • <kbd>Entf</kbd> zum Löschen • Klicken Sie auf einen anderen Eingang zum Umleiten
|
{t('Verbindungspfeil ausgewählt')}
|
||||||
|
{' · '}
|
||||||
|
<kbd>Entf</kbd> {t('zum Löschen')}
|
||||||
|
{' · '}
|
||||||
|
{t('Anderen Eingang anklicken zum Umleiten')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|
@ -840,7 +853,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
||||||
)}
|
)}
|
||||||
{nodes.length === 0 && (
|
{nodes.length === 0 && (
|
||||||
<div className={styles.canvasPlaceholder}>
|
<div className={styles.canvasPlaceholder}>
|
||||||
<p>{t('Nodes aus der Liste links')}</p>
|
<p>{t('Knoten aus der Liste links ziehen')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
placeholder={t('z.B. Kundenformular prüfen, Land')}
|
placeholder={t('z.B. Kundenformular prüfen, Land')}
|
||||||
/>
|
/>
|
||||||
<p className={styles.nodeConfigNameHint}>
|
<p className={styles.nodeConfigNameHint}>
|
||||||
Wird im Data Picker angezeigt, um diesen Node zu identifizieren.
|
{t('Wird im Data Picker angezeigt, um diesen Node zu identifizieren.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({ nodeTypes,
|
||||||
return (
|
return (
|
||||||
<div className={styles.sidebar} style={style}>
|
<div className={styles.sidebar} style={style}>
|
||||||
<div className={styles.sidebarHeader}>
|
<div className={styles.sidebarHeader}>
|
||||||
<h3 className={styles.sidebarTitle}>Nodes</h3>
|
<h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={styles.sidebarSearch}
|
className={styles.sidebarSearch}
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
||||||
if (!runId) {
|
if (!runId) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '16px', color: 'var(--text-secondary, #888)', fontSize: '13px' }}>
|
<div style={{ padding: '16px', color: 'var(--text-secondary, #888)', fontSize: '13px' }}>
|
||||||
Select a run to see tracing details.
|
{t('Run auswählen, um Tracing-Details zu sehen.')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +180,10 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '12px', overflowY: 'auto', height: '100%' }}>
|
<div style={{ padding: '12px', overflowY: 'auto', height: '100%' }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '12px' }}>
|
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '12px' }}>
|
||||||
Run Steps {loading && <span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>(loading...)</span>}
|
{t('Run-Schritte')}{' '}
|
||||||
|
{loading && (
|
||||||
|
<span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>({t('wird geladen…')})</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{steps.length === 0 && !loading && (
|
{steps.length === 0 && !loading && (
|
||||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>{t('Noch keine Schritte aufgezeichnet')}</div>
|
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>{t('Noch keine Schritte aufgezeichnet')}</div>
|
||||||
|
|
@ -223,7 +226,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
||||||
<span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
<span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
{step.retryCount > 0 && (
|
{step.retryCount > 0 && (
|
||||||
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('Wiederholungsanzahl')}>
|
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('Wiederholungsanzahl')}>
|
||||||
{step.retryCount}x retry
|
{step.retryCount}x {t('Wiederholung')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{step.durationMs != null && (
|
{step.durationMs != null && (
|
||||||
|
|
@ -244,11 +247,13 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
||||||
<div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div>
|
<div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div>
|
||||||
)}
|
)}
|
||||||
{step.tokensUsed > 0 && (
|
{step.tokensUsed > 0 && (
|
||||||
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>{step.tokensUsed} tokens</div>
|
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>
|
||||||
|
{step.tokensUsed} {t('Tokens')}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CollapsibleSection label="Input" content={inputStr} />
|
<CollapsibleSection label={t('Eingabe')} content={inputStr} />
|
||||||
<CollapsibleSection label="Output" content={outputStr} />
|
<CollapsibleSection label={t('Ausgabe')} content={outputStr} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* TemplatePicker - modal to browse and select a workflow template for creating a new workflow.
|
* TemplatePicker - modal to browse and select a workflow template for creating a new workflow.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { FaSpinner } from 'react-icons/fa';
|
import { FaSpinner } from 'react-icons/fa';
|
||||||
import {
|
import {
|
||||||
fetchTemplates,
|
fetchTemplates,
|
||||||
|
|
@ -11,14 +11,7 @@ import {
|
||||||
type ApiRequestFunction,
|
type ApiRequestFunction,
|
||||||
} from '../../../api/workflowApi';
|
} from '../../../api/workflowApi';
|
||||||
import styles from './Automation2FlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
const SCOPE_LABELS: Record<AutoTemplateScope | 'all', string> = {
|
|
||||||
all: 'Alle',
|
|
||||||
user: 'Meine',
|
|
||||||
instance: 'Instanz',
|
|
||||||
mandate: 'Mandant',
|
|
||||||
system: 'System',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TemplatePickerProps {
|
interface TemplatePickerProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -35,6 +28,18 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
instanceId,
|
instanceId,
|
||||||
request,
|
request,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const scopeLabels = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
all: t('Alle'),
|
||||||
|
user: t('Meine'),
|
||||||
|
instance: t('Instanz'),
|
||||||
|
mandate: t('Mandant'),
|
||||||
|
system: t('System'),
|
||||||
|
}) as Record<AutoTemplateScope | 'all', string>,
|
||||||
|
[t]
|
||||||
|
);
|
||||||
const [templates, setTemplates] = useState<AutoWorkflowTemplate[]>([]);
|
const [templates, setTemplates] = useState<AutoWorkflowTemplate[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeScope, setActiveScope] = useState<AutoTemplateScope | 'all'>('all');
|
const [activeScope, setActiveScope] = useState<AutoTemplateScope | 'all'>('all');
|
||||||
|
|
@ -76,10 +81,10 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
<div className={styles.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="tpl-picker-title">
|
<div className={styles.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="tpl-picker-title">
|
||||||
<div className={styles.workflowModal} style={{ maxWidth: 600, maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}>
|
<div className={styles.workflowModal} style={{ maxWidth: 600, maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
<h3 id="tpl-picker-title" className={styles.workflowModalTitle}>
|
<h3 id="tpl-picker-title" className={styles.workflowModalTitle}>
|
||||||
Neu aus Vorlage
|
{t('Neu aus Vorlage')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={styles.workflowModalHint}>
|
<p className={styles.workflowModalHint}>
|
||||||
Wählen Sie eine Vorlage, um einen neuen Workflow zu erstellen.
|
{t('Wählen Sie eine Vorlage, um einen neuen Workflow zu erstellen.')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||||
|
|
@ -91,7 +96,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
onClick={() => setActiveScope(s)}
|
onClick={() => setActiveScope(s)}
|
||||||
style={{ fontSize: '0.8rem', padding: '4px 10px' }}
|
style={{ fontSize: '0.8rem', padding: '4px 10px' }}
|
||||||
>
|
>
|
||||||
{SCOPE_LABELS[s]}
|
{scopeLabels[s]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,14 +108,14 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
</div>
|
</div>
|
||||||
) : templates.length === 0 ? (
|
) : templates.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: 24, color: 'var(--text-secondary, #888)' }}>
|
<div style={{ textAlign: 'center', padding: 24, color: 'var(--text-secondary, #888)' }}>
|
||||||
Keine Vorlagen gefunden.
|
{t('Keine Vorlagen gefunden.')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: '2px solid var(--border-color, #e0e0e0)', textAlign: 'left' }}>
|
<tr style={{ borderBottom: '2px solid var(--border-color, #e0e0e0)', textAlign: 'left' }}>
|
||||||
<th style={{ padding: '6px 8px' }}>Name</th>
|
<th style={{ padding: '6px 8px' }}>{t('Name')}</th>
|
||||||
<th style={{ padding: '6px 8px', width: 80 }}>Scope</th>
|
<th style={{ padding: '6px 8px', width: 80 }}>{t('Scope')}</th>
|
||||||
<th style={{ padding: '6px 8px', width: 100 }}></th>
|
<th style={{ padding: '6px 8px', width: 100 }}></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -119,7 +124,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
<tr key={tpl.id} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}>
|
<tr key={tpl.id} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}>
|
||||||
<td style={{ padding: '8px' }}>{tpl.label}</td>
|
<td style={{ padding: '8px' }}>{tpl.label}</td>
|
||||||
<td style={{ padding: '8px', fontSize: '0.8rem', color: 'var(--text-secondary, #888)' }}>
|
<td style={{ padding: '8px', fontSize: '0.8rem', color: 'var(--text-secondary, #888)' }}>
|
||||||
{SCOPE_LABELS[(tpl.templateScope as AutoTemplateScope) || 'user']}
|
{scopeLabels[(tpl.templateScope as AutoTemplateScope) || 'user']}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '8px', textAlign: 'right' }}>
|
<td style={{ padding: '8px', textAlign: 'right' }}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -129,7 +134,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
onClick={() => _handleSelect(tpl.id)}
|
onClick={() => _handleSelect(tpl.id)}
|
||||||
disabled={copying !== null}
|
disabled={copying !== null}
|
||||||
>
|
>
|
||||||
{copying === tpl.id ? <FaSpinner className={styles.spinner} /> : 'Übernehmen'}
|
{copying === tpl.id ? <FaSpinner className={styles.spinner} /> : t('Übernehmen')}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -141,7 +146,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
|
|
||||||
<div className={styles.workflowModalActions} style={{ marginTop: 12 }}>
|
<div className={styles.workflowModalActions} style={{ marginTop: 12 }}>
|
||||||
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
|
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
|
||||||
Abbrechen
|
{t('Abbrechen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const label =
|
const label =
|
||||||
titleDe.trim() || kindOptions.find((o) => o.value === kind)?.label || 'Start';
|
titleDe.trim() || kindOptions.find((o) => o.value === kind)?.label || t('Start');
|
||||||
const next = buildInvocationsForPrimaryKind(kind, invocations, label);
|
const next = buildInvocationsForPrimaryKind(kind, invocations, label);
|
||||||
onApply(next);
|
onApply(next);
|
||||||
onClose();
|
onClose();
|
||||||
|
|
@ -74,15 +74,16 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
||||||
<div className={styles.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="wf-cfg-title">
|
<div className={styles.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="wf-cfg-title">
|
||||||
<div className={styles.workflowModal}>
|
<div className={styles.workflowModal}>
|
||||||
<h3 id="wf-cfg-title" className={styles.workflowModalTitle}>
|
<h3 id="wf-cfg-title" className={styles.workflowModalTitle}>
|
||||||
Workflow-Konfiguration
|
{t('Workflow-Konfiguration')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={styles.workflowModalHint}>
|
<p className={styles.workflowModalHint}>
|
||||||
Legen Sie fest, wie dieser Workflow gestartet werden soll. Die Start-Node im Editor passt sich dem
|
{t(
|
||||||
gewählten Einstieg an (z. B. Formular-Felder auf der Start-Node bearbeiten).
|
'Legen Sie fest, wie dieser Workflow gestartet werden soll. Die Start-Node im Editor passt sich dem gewählten Einstieg an (z. B. Formular-Felder auf der Start-Node bearbeiten).'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<label className={styles.workflowModalLabel} htmlFor="wf-start-title">
|
<label className={styles.workflowModalLabel} htmlFor="wf-start-title">
|
||||||
Titel der Start Node
|
{t('Titel der Start Node')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="wf-start-title"
|
id="wf-start-title"
|
||||||
|
|
@ -92,7 +93,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
||||||
placeholder={t('z.B. Angebot anlegen')}
|
placeholder={t('z.B. Angebot anlegen')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.workflowModalRadioGroup} role="radiogroup" aria-label="Einstiegsart">
|
<div className={styles.workflowModalRadioGroup} role="radiogroup" aria-label={t('Einstiegsart')}>
|
||||||
{kindOptions.map((o) => (
|
{kindOptions.map((o) => (
|
||||||
<label key={o.value} className={styles.workflowModalRadio}>
|
<label key={o.value} className={styles.workflowModalRadio}>
|
||||||
<input
|
<input
|
||||||
|
|
@ -109,10 +110,10 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
||||||
|
|
||||||
<div className={styles.workflowModalActions}>
|
<div className={styles.workflowModalActions}>
|
||||||
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
|
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
|
||||||
Abbrechen
|
{t('Abbrechen')}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className={styles.workflowModalBtnPrimary}>
|
<button type="submit" className={styles.workflowModalBtnPrimary}>
|
||||||
Übernehmen
|
{t('Übernehmen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label>Felder</label>
|
<label>{t('Felder')}</label>
|
||||||
<div className={styles.formFieldsList}>
|
<div className={styles.formFieldsList}>
|
||||||
{fields.map((f, i) => (
|
{fields.map((f, i) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -87,7 +87,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.formFieldInputs}>
|
<div className={styles.formFieldInputs}>
|
||||||
<input
|
<input
|
||||||
placeholder="name"
|
placeholder={t('name')}
|
||||||
value={f.name ?? ''}
|
value={f.name ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = [...fields];
|
const next = [...fields];
|
||||||
|
|
@ -96,7 +96,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
placeholder="label"
|
placeholder={t('label')}
|
||||||
value={f.label ?? ''}
|
value={f.label ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = [...fields];
|
const next = [...fields];
|
||||||
|
|
@ -111,13 +111,13 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
value={f.type ?? 'string'}
|
value={f.type ?? 'string'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = [...fields];
|
const next = [...fields];
|
||||||
const t = e.target.value;
|
const fieldType = e.target.value;
|
||||||
next[i] = {
|
next[i] = {
|
||||||
...next[i],
|
...next[i],
|
||||||
type: t,
|
type: fieldType,
|
||||||
...(t === 'clickup_tasks'
|
...(fieldType === 'clickup_tasks'
|
||||||
? { clickupStatusOptions: undefined }
|
? { clickupStatusOptions: undefined }
|
||||||
: t === 'clickup_status'
|
: fieldType === 'clickup_status'
|
||||||
? { clickupConnectionId: undefined, clickupListId: undefined }
|
? { clickupConnectionId: undefined, clickupListId: undefined }
|
||||||
: {
|
: {
|
||||||
clickupConnectionId: undefined,
|
clickupConnectionId: undefined,
|
||||||
|
|
@ -129,10 +129,10 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
}}
|
}}
|
||||||
style={{ width: 'auto', minWidth: 90 }}
|
style={{ width: 'auto', minWidth: 90 }}
|
||||||
>
|
>
|
||||||
<option value="string">Text</option>
|
<option value="string">{t('Text')}</option>
|
||||||
<option value="number">Number</option>
|
<option value="number">{t('Zahl')}</option>
|
||||||
<option value="date">Date</option>
|
<option value="date">{t('Datum')}</option>
|
||||||
<option value="boolean">Checkbox</option>
|
<option value="boolean">{t('Kontrollkästchen')}</option>
|
||||||
<option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option>
|
<option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option>
|
||||||
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
|
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -146,7 +146,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
updateParam('fields', next);
|
updateParam('fields', next);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
Pflichtfeld
|
{t('Pflichtfeld')}
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -161,13 +161,16 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%', fontSize: '0.8rem', color: 'var(--text-secondary, #666)' }}>
|
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%', fontSize: '0.8rem', color: 'var(--text-secondary, #666)' }}>
|
||||||
{Array.isArray(f.clickupStatusOptions) && f.clickupStatusOptions.length > 0 ? (
|
{Array.isArray(f.clickupStatusOptions) && f.clickupStatusOptions.length > 0 ? (
|
||||||
<p style={{ margin: '0 0 6px' }}>
|
<p style={{ margin: '0 0 6px' }}>
|
||||||
Dropdown mit {f.clickupStatusOptions.length} Status aus der ClickUp-Liste (Wert = exakter
|
{t(
|
||||||
Status-Name für die API).
|
'Dropdown mit {count} Status aus der ClickUp-Liste (Wert = exakter Status-Name für die API).',
|
||||||
|
{ count: String(f.clickupStatusOptions.length) }
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ margin: '0 0 6px' }}>
|
<p style={{ margin: '0 0 6px' }}>
|
||||||
Keine Optionen — im ClickUp-Knoten „Aufgabe erstellen“ Liste wählen und „Formular mit Liste
|
{t(
|
||||||
abgleichen“.
|
'Keine Optionen — im ClickUp-Knoten „Aufgabe erstellen“ Liste wählen und „Formular mit Liste abgleichen“.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -175,7 +178,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
{f.type === 'clickup_tasks' ? (
|
{f.type === 'clickup_tasks' ? (
|
||||||
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%' }}>
|
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%' }}>
|
||||||
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
|
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
|
||||||
ClickUp-Verbindung
|
{t('ClickUp-Verbindung')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={f.clickupConnectionId ?? ''}
|
value={f.clickupConnectionId ?? ''}
|
||||||
|
|
@ -187,7 +190,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
disabled={connectionsLoading || !instanceId}
|
disabled={connectionsLoading || !instanceId}
|
||||||
style={{ width: '100%', marginBottom: 8 }}
|
style={{ width: '100%', marginBottom: 8 }}
|
||||||
>
|
>
|
||||||
<option value="">{connectionsLoading ? 'Lade…' : 'Verbindung wählen…'}</option>
|
<option value="">{connectionsLoading ? t('Lade…') : t('Verbindung wählen…')}</option>
|
||||||
{connections.map((c) => (
|
{connections.map((c) => (
|
||||||
<option key={c.id} value={c.id}>
|
<option key={c.id} value={c.id}>
|
||||||
{c.externalUsername ?? c.id}
|
{c.externalUsername ?? c.id}
|
||||||
|
|
@ -195,7 +198,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
|
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
|
||||||
Listen-ID (verknüpfte Liste / Ziel-Liste)
|
{t('Listen-ID (verknüpfte Liste / Ziel-Liste)')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
placeholder={t('z.B. aus ClickUp-URL: list/123456789')}
|
placeholder={t('z.B. aus ClickUp-URL: list/123456789')}
|
||||||
|
|
@ -208,9 +211,9 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginTop: 6 }}>
|
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginTop: 6 }}>
|
||||||
Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:{' '}
|
{t('Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:')}{' '}
|
||||||
<code>{'{ add: [taskId], rem: [] }'}</code> — im ClickUp-Node per Datenquelle auf das
|
<code>{'{ add: [taskId], rem: [] }'}</code>{' '}
|
||||||
Formularfeld mappen.
|
{t('— im ClickUp-Node per Datenquelle auf das Formularfeld mappen.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -222,7 +225,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
updateParam('fields', [...fields, { name: '', type: 'string', label: '', required: false }])
|
updateParam('fields', [...fields, { name: '', type: 'string', label: '', required: false }])
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
+ Feld
|
+ {t('Feld')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
|
||||||
};
|
};
|
||||||
|
|
||||||
const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, allParams }) => {
|
const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, allParams }) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const dependsOn = param.frontendOptions?.dependsOn as string | undefined;
|
const dependsOn = param.frontendOptions?.dependsOn as string | undefined;
|
||||||
const depValue = dependsOn ? allParams?.[dependsOn] : undefined;
|
const depValue = dependsOn ? allParams?.[dependsOn] : undefined;
|
||||||
const disabled = dependsOn && !depValue;
|
const disabled = dependsOn && !depValue;
|
||||||
|
|
@ -191,7 +192,7 @@ const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, al
|
||||||
value={typeof value === 'string' ? value : ''}
|
value={typeof value === 'string' ? value : ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
placeholder={disabled ? `Select ${dependsOn} first` : param.name}
|
placeholder={disabled ? t('Zuerst {field} wählen', { field: dependsOn ?? '' }) : param.name}
|
||||||
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', opacity: disabled ? 0.5 : 1 }}
|
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', opacity: disabled ? 0.5 : 1 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -214,9 +215,9 @@ const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }
|
||||||
{cases.map((c: Record<string, unknown>, i: number) => (
|
{cases.map((c: Record<string, unknown>, i: number) => (
|
||||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||||
<select value={String(c.operator || 'eq')} onChange={(e) => updateCase(i, 'operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
<select value={String(c.operator || 'eq')} onChange={(e) => updateCase(i, 'operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||||
<option value="eq">equals</option>
|
<option value="eq">{t('ist gleich')}</option>
|
||||||
<option value="neq">{t('ungleich')}</option>
|
<option value="neq">{t('ungleich')}</option>
|
||||||
<option value="contains">contains</option>
|
<option value="contains">{t('enthält')}</option>
|
||||||
<option value="gt">{t('größer als')}</option>
|
<option value="gt">{t('größer als')}</option>
|
||||||
<option value="lt">{t('kleiner als')}</option>
|
<option value="lt">{t('kleiner als')}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -244,18 +245,18 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
||||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||||
{fields.map((f: Record<string, unknown>, i: number) => (
|
{fields.map((f: Record<string, unknown>, i: number) => (
|
||||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4, alignItems: 'center' }}>
|
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4, alignItems: 'center' }}>
|
||||||
<input type="text" placeholder="Name" value={String(f.name ?? '')} onChange={(e) => updateField(i, 'name', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
<input type="text" placeholder={t('Name')} value={String(f.name ?? '')} onChange={(e) => updateField(i, 'name', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||||
<select value={String(f.type ?? 'text')} onChange={(e) => updateField(i, 'type', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
<select value={String(f.type ?? 'text')} onChange={(e) => updateField(i, 'type', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||||
<option value="text">Text</option>
|
<option value="text">{t('Text')}</option>
|
||||||
<option value="number">Number</option>
|
<option value="number">{t('Zahl')}</option>
|
||||||
<option value="date">Date</option>
|
<option value="date">{t('Datum')}</option>
|
||||||
<option value="checkbox">Checkbox</option>
|
<option value="checkbox">{t('Kontrollkästchen')}</option>
|
||||||
<option value="select">Select</option>
|
<option value="select">{t('Auswahl')}</option>
|
||||||
<option value="textarea">Textarea</option>
|
<option value="textarea">{t('Mehrzeilig')}</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" placeholder="Label" value={String(f.label ?? '')} onChange={(e) => updateField(i, 'label', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
<input type="text" placeholder={t('Bezeichnung')} value={String(f.label ?? '')} onChange={(e) => updateField(i, 'label', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||||
<label style={{ fontSize: 11, display: 'flex', alignItems: 'center', gap: 2 }}>
|
<label style={{ fontSize: 11, display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<input type="checkbox" checked={Boolean(f.required)} onChange={(e) => updateField(i, 'required', e.target.checked)} /> Req
|
<input type="checkbox" checked={Boolean(f.required)} onChange={(e) => updateField(i, 'required', e.target.checked)} /> {t('Pflicht')}
|
||||||
</label>
|
</label>
|
||||||
<button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
<button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -280,8 +281,8 @@ const KeyValueRowsEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
||||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||||
{rows.map((r: Record<string, unknown>, i: number) => (
|
{rows.map((r: Record<string, unknown>, i: number) => (
|
||||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||||
<input type="text" placeholder="Key" value={String(r.key ?? r.fieldKey ?? '')} onChange={(e) => updateRow(i, 'key', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
<input type="text" placeholder={t('Schlüssel')} value={String(r.key ?? r.fieldKey ?? '')} onChange={(e) => updateRow(i, 'key', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||||
<input type="text" placeholder="Value" value={String(r.value ?? '')} onChange={(e) => updateRow(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
<input type="text" placeholder={t('Wert')} value={String(r.value ?? '')} onChange={(e) => updateRow(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||||
<button onClick={() => removeRow(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
<button onClick={() => removeRow(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -299,7 +300,7 @@ const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) =
|
||||||
type="text"
|
type="text"
|
||||||
value={typeof value === 'string' ? value : ''}
|
value={typeof value === 'string' ? value : ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={t('index.5')}
|
placeholder={t('0 9 * * *')}
|
||||||
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }}
|
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }}
|
||||||
/>
|
/>
|
||||||
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('Cron: Min Stunde Tag Monat')}</p>
|
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('Cron: Min Stunde Tag Monat')}</p>
|
||||||
|
|
@ -316,17 +317,17 @@ const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange
|
||||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||||
<option value="eq">equals</option>
|
<option value="eq">{t('ist gleich')}</option>
|
||||||
<option value="neq">{t('ungleich')}</option>
|
<option value="neq">{t('ungleich')}</option>
|
||||||
<option value="gt">{t('größer als')}</option>
|
<option value="gt">{t('größer als')}</option>
|
||||||
<option value="lt">{t('kleiner als')}</option>
|
<option value="lt">{t('kleiner als')}</option>
|
||||||
<option value="contains">contains</option>
|
<option value="contains">{t('enthält')}</option>
|
||||||
<option value="empty">{t('ist leer')}</option>
|
<option value="empty">{t('ist leer')}</option>
|
||||||
<option value="not_empty">{t('ist nicht leer')}</option>
|
<option value="not_empty">{t('ist nicht leer')}</option>
|
||||||
<option value="is_true">{t('ist wahr')}</option>
|
<option value="is_true">{t('ist wahr')}</option>
|
||||||
<option value="is_false">{t('ist falsch')}</option>
|
<option value="is_false">{t('ist falsch')}</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
<input type="text" placeholder={t('Wert')} value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -366,18 +367,18 @@ const FilterExpressionEditor: React.FC<FieldRendererProps> = ({ param, value, on
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
<input type="text" placeholder="Field" value={String(cond.field ?? '')} onChange={(e) => update('field', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
<input type="text" placeholder={t('Feld')} value={String(cond.field ?? '')} onChange={(e) => update('field', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||||
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||||
<option value="eq">equals</option>
|
<option value="eq">{t('ist gleich')}</option>
|
||||||
<option value="neq">{t('ungleich')}</option>
|
<option value="neq">{t('ungleich')}</option>
|
||||||
<option value="contains">contains</option>
|
<option value="contains">{t('enthält')}</option>
|
||||||
<option value="startsWith">{t('beginnt mit')}</option>
|
<option value="startsWith">{t('beginnt mit')}</option>
|
||||||
<option value="isEmpty">{t('ist leer')}</option>
|
<option value="isEmpty">{t('ist leer')}</option>
|
||||||
<option value="isNotEmpty">{t('ist nicht leer')}</option>
|
<option value="isNotEmpty">{t('ist nicht leer')}</option>
|
||||||
<option value="gt">{t('größer als')}</option>
|
<option value="gt">{t('größer als')}</option>
|
||||||
<option value="lt">{t('kleiner als')}</option>
|
<option value="lt">{t('kleiner als')}</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
<input type="text" placeholder={t('Wert')} value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
||||||
return (
|
return (
|
||||||
<div className={styles.ifElseConditionEditor}>
|
<div className={styles.ifElseConditionEditor}>
|
||||||
<div className={styles.ifElseConditionRow}>
|
<div className={styles.ifElseConditionRow}>
|
||||||
<label>Datenquelle</label>
|
<label>{t('Datenquelle')}</label>
|
||||||
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} />
|
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.ifElseConditionRow}>
|
<div className={styles.ifElseConditionRow}>
|
||||||
|
|
@ -114,7 +114,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
||||||
</div>
|
</div>
|
||||||
{needsValue && (
|
{needsValue && (
|
||||||
<div className={styles.ifElseConditionRow}>
|
<div className={styles.ifElseConditionRow}>
|
||||||
<label>Wert</label>
|
<label>{t('Wert')}</label>
|
||||||
{mimeTypeOptions.length > 0 ? (
|
{mimeTypeOptions.length > 0 ? (
|
||||||
<select
|
<select
|
||||||
value={String(value ?? '')}
|
value={String(value ?? '')}
|
||||||
|
|
|
||||||
|
|
@ -45,15 +45,19 @@ function _buildPathsFromSchema(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _buildPathsFromPreview(obj: unknown, basePath: (string | number)[] = []): PickablePath[] {
|
function _buildPathsFromPreview(
|
||||||
const pathLabel = basePath.length ? basePath.map(String).join(' → ') : '(ganze Ausgabe)';
|
obj: unknown,
|
||||||
|
basePath: (string | number)[] = [],
|
||||||
|
wholeOutputLabel = '(ganze Ausgabe)',
|
||||||
|
): PickablePath[] {
|
||||||
|
const pathLabel = basePath.length ? basePath.map(String).join(' → ') : wholeOutputLabel;
|
||||||
if (obj == null || typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
|
if (obj == null || typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
|
||||||
return [{ path: [...basePath], label: pathLabel }];
|
return [{ path: [...basePath], label: pathLabel }];
|
||||||
}
|
}
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
|
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
|
||||||
for (let i = 0; i < Math.min(obj.length, 5); i++) {
|
for (let i = 0; i < Math.min(obj.length, 5); i++) {
|
||||||
result.push(..._buildPathsFromPreview(obj[i], [...basePath, i]));
|
result.push(..._buildPathsFromPreview(obj[i], [...basePath, i], wholeOutputLabel));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +65,7 @@ function _buildPathsFromPreview(obj: unknown, basePath: (string | number)[] = []
|
||||||
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
|
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
|
||||||
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
||||||
if (k.startsWith('_')) continue;
|
if (k.startsWith('_')) continue;
|
||||||
result.push(..._buildPathsFromPreview(v, [...basePath, k]));
|
result.push(..._buildPathsFromPreview(v, [...basePath, k], wholeOutputLabel));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +164,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||||
onClick={() => setShowSystem(!showSystem)}
|
onClick={() => setShowSystem(!showSystem)}
|
||||||
>
|
>
|
||||||
<span className={styles.dataPickerExpandIcon}>{showSystem ? '▼' : '▶'}</span>
|
<span className={styles.dataPickerExpandIcon}>{showSystem ? '▼' : '▶'}</span>
|
||||||
<span className={styles.dataPickerNodeLabel}>System</span>
|
<span className={styles.dataPickerNodeLabel}>{t('System')}</span>
|
||||||
</button>
|
</button>
|
||||||
{showSystem && (
|
{showSystem && (
|
||||||
<div className={styles.dataPickerTree}>
|
<div className={styles.dataPickerTree}>
|
||||||
|
|
@ -200,7 +204,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||||
const schemaPaths = _buildPathsFromSchema(resolvedSchema);
|
const schemaPaths = _buildPathsFromSchema(resolvedSchema);
|
||||||
const paths = schemaPaths.length > 0
|
const paths = schemaPaths.length > 0
|
||||||
? schemaPaths
|
? schemaPaths
|
||||||
: _buildPathsFromPreview(nodeOutputsPreview[nodeId]);
|
: _buildPathsFromPreview(nodeOutputsPreview[nodeId], [], t('(ganze Ausgabe)'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={nodeId} className={styles.dataPickerNodeSection}>
|
<div key={nodeId} className={styles.dataPickerNodeSection}>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ function buildLoopOptions(
|
||||||
sourceIds: string[],
|
sourceIds: string[],
|
||||||
nodes: Array<{ id: string; type?: string; title?: string; parameters?: Record<string, unknown> }>,
|
nodes: Array<{ id: string; type?: string; title?: string; parameters?: Record<string, unknown> }>,
|
||||||
nodeOutputsPreview: Record<string, unknown>,
|
nodeOutputsPreview: Record<string, unknown>,
|
||||||
getNodeLabel: (n: { id: string; type?: string; title?: string }) => string
|
getNodeLabel: (n: { id: string; type?: string; title?: string }) => string,
|
||||||
|
translate: (key: string) => string
|
||||||
): LoopOption[] {
|
): LoopOption[] {
|
||||||
const options: LoopOption[] = [];
|
const options: LoopOption[] = [];
|
||||||
|
|
||||||
|
|
@ -50,13 +51,13 @@ function buildLoopOptions(
|
||||||
if (node?.type === 'trigger.form') {
|
if (node?.type === 'trigger.form') {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['payload']),
|
ref: createRef(nodeId, ['payload']),
|
||||||
label: `Alle Formularfelder (${nodeLabel})`,
|
label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
const filesVal = getValueAtPath(preview, ['files']);
|
const filesVal = getValueAtPath(preview, ['files']);
|
||||||
if (Array.isArray(filesVal)) {
|
if (Array.isArray(filesVal)) {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['files']),
|
ref: createRef(nodeId, ['files']),
|
||||||
label: `Alle Dateien aus Formular (${nodeLabel})`,
|
label: `${translate('Alle Dateien aus Formular')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -65,7 +66,7 @@ function buildLoopOptions(
|
||||||
if (node?.type === 'input.form') {
|
if (node?.type === 'input.form') {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, []),
|
ref: createRef(nodeId, []),
|
||||||
label: `Alle Formularfelder (${nodeLabel})`,
|
label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -73,11 +74,11 @@ function buildLoopOptions(
|
||||||
if (node?.type === 'input.upload') {
|
if (node?.type === 'input.upload') {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['files']),
|
ref: createRef(nodeId, ['files']),
|
||||||
label: `Alle hochgeladenen Dateien (${nodeLabel})`,
|
label: `${translate('Alle hochgeladenen Dateien')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['fileIds']),
|
ref: createRef(nodeId, ['fileIds']),
|
||||||
label: `Alle Datei-IDs (${nodeLabel})`,
|
label: `${translate('Alle Datei-IDs')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +86,7 @@ function buildLoopOptions(
|
||||||
if (node?.type === 'flow.loop') {
|
if (node?.type === 'flow.loop') {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['items']),
|
ref: createRef(nodeId, ['items']),
|
||||||
label: `Alle Elemente aus Schleife (${nodeLabel})`,
|
label: `${translate('Alle Elemente aus Schleife')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +94,7 @@ function buildLoopOptions(
|
||||||
if (node?.type === 'email.searchEmail') {
|
if (node?.type === 'email.searchEmail') {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['data', 'searchResults', 'results']),
|
ref: createRef(nodeId, ['data', 'searchResults', 'results']),
|
||||||
label: `Alle gefundenen E-Mails (${nodeLabel})`,
|
label: `${translate('Alle gefundenen E-Mails')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +102,7 @@ function buildLoopOptions(
|
||||||
if (node?.type === 'email.checkEmail') {
|
if (node?.type === 'email.checkEmail') {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['data', 'emails', 'emails']),
|
ref: createRef(nodeId, ['data', 'emails', 'emails']),
|
||||||
label: `Alle E-Mails (${nodeLabel})`,
|
label: `${translate('Alle E-Mails')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +110,7 @@ function buildLoopOptions(
|
||||||
if (node?.type === 'sharepoint.listFiles') {
|
if (node?.type === 'sharepoint.listFiles') {
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, ['files']),
|
ref: createRef(nodeId, ['files']),
|
||||||
label: `Alle Dateien (${nodeLabel})`,
|
label: `${translate('Alle Dateien')} (${nodeLabel})`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +158,7 @@ interface LoopItemsSelectProps {
|
||||||
|
|
||||||
export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = 'Über was soll iteriert werden?',
|
placeholder,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useAutomation2DataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
|
|
@ -167,7 +168,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
||||||
if (sourceIds.length === 0) {
|
if (sourceIds.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className={styles.dynamicValueEmptyHint}>
|
<p className={styles.dynamicValueEmptyHint}>
|
||||||
Keine vorherigen Nodes verbunden. Verbinden Sie zuerst Nodes mit der Schleife.
|
{t('Keine vorherigen Nodes verbunden. Verbinden Sie zuerst Nodes mit der Schleife.')}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +177,8 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
||||||
sourceIds,
|
sourceIds,
|
||||||
dataFlow.nodes,
|
dataFlow.nodes,
|
||||||
dataFlow.nodeOutputsPreview,
|
dataFlow.nodeOutputsPreview,
|
||||||
dataFlow.getNodeLabel
|
dataFlow.getNodeLabel,
|
||||||
|
t
|
||||||
);
|
);
|
||||||
|
|
||||||
const ref = isRef(value) ? value : null;
|
const ref = isRef(value) ? value : null;
|
||||||
|
|
@ -198,7 +200,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
||||||
}}
|
}}
|
||||||
className={styles.startsInput}
|
className={styles.startsInput}
|
||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<option value="">{placeholder ?? t('Über was soll iteriert werden?')}</option>
|
||||||
{options.map((o) => (
|
{options.map((o) => (
|
||||||
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
||||||
{o.label}
|
{o.label}
|
||||||
|
|
@ -206,7 +208,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className={styles.nodeConfigNameHint}>
|
<p className={styles.nodeConfigNameHint}>
|
||||||
Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.
|
{t('Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRef, isRef, isValue, createValue, type DataRef } from './dataRef';
|
import { createRef, isRef, isValue, createValue, type DataRef } from './dataRef';
|
||||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
/** How to build path options for StatischKontextSelect / RefSourceSelect. */
|
/** How to build path options for StatischKontextSelect / RefSourceSelect. */
|
||||||
export type PathPickMode = 'default' | 'clickup_task_id' | 'exclude_forms';
|
export type PathPickMode = 'default' | 'clickup_task_id' | 'exclude_forms';
|
||||||
|
|
@ -131,6 +132,11 @@ export function refToOptionValue(ref: DataRef): string {
|
||||||
return JSON.stringify(ref);
|
return JSON.stringify(ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _pathLabelForDisplay(pathLabel: string, translate: (key: string) => string): string {
|
||||||
|
if (pathLabel === 'Aufgaben-ID') return translate('Aufgaben-ID');
|
||||||
|
return pathLabel;
|
||||||
|
}
|
||||||
|
|
||||||
export function optionValueToRef(s: string): DataRef | null {
|
export function optionValueToRef(s: string): DataRef | null {
|
||||||
try {
|
try {
|
||||||
const o = JSON.parse(s) as unknown;
|
const o = JSON.parse(s) as unknown;
|
||||||
|
|
@ -190,10 +196,11 @@ interface StatischKontextSelectProps {
|
||||||
export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
|
export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = '— Quelle wählen —',
|
placeholder,
|
||||||
staticLabel = 'Statisch',
|
staticLabel,
|
||||||
pathPickMode = 'default',
|
pathPickMode = 'default',
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const dataFlow = useAutomation2DataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
if (!dataFlow) return null;
|
if (!dataFlow) return null;
|
||||||
|
|
||||||
|
|
@ -213,7 +220,8 @@ export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
|
||||||
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
|
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
|
||||||
const paths = pickPathsForNode(node, preview, pathPickMode);
|
const paths = pickPathsForNode(node, preview, pathPickMode);
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
const displayLabel = p.pathLabel ? `${nodeLabel} → ${p.pathLabel}` : nodeLabel;
|
const pathLabelUi = _pathLabelForDisplay(p.pathLabel, t);
|
||||||
|
const displayLabel = pathLabelUi ? `${nodeLabel} → ${pathLabelUi}` : nodeLabel;
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, p.path),
|
ref: createRef(nodeId, p.path),
|
||||||
label: displayLabel,
|
label: displayLabel,
|
||||||
|
|
@ -245,8 +253,8 @@ export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
|
||||||
if (ref) onChange(ref);
|
if (ref) onChange(ref);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<option value="">{placeholder ?? t('— Quelle wählen —')}</option>
|
||||||
<option value={STATIC_SOURCE_VALUE}>{staticLabel}</option>
|
<option value={STATIC_SOURCE_VALUE}>{staticLabel ?? t('Statisch')}</option>
|
||||||
{options.map((o) => (
|
{options.map((o) => (
|
||||||
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
||||||
{o.label}
|
{o.label}
|
||||||
|
|
@ -267,9 +275,10 @@ interface RefSourceSelectProps {
|
||||||
export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
|
export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = 'Datenquelle wählen…',
|
placeholder,
|
||||||
pathPickMode = 'default',
|
pathPickMode = 'default',
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const dataFlow = useAutomation2DataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
if (!dataFlow) return null;
|
if (!dataFlow) return null;
|
||||||
|
|
||||||
|
|
@ -289,7 +298,8 @@ export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
|
||||||
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
|
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
|
||||||
const paths = pickPathsForNode(node, preview, pathPickMode);
|
const paths = pickPathsForNode(node, preview, pathPickMode);
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
const displayLabel = p.pathLabel ? `${nodeLabel} → ${p.pathLabel}` : nodeLabel;
|
const pathLabelUi = _pathLabelForDisplay(p.pathLabel, t);
|
||||||
|
const displayLabel = pathLabelUi ? `${nodeLabel} → ${pathLabelUi}` : nodeLabel;
|
||||||
options.push({
|
options.push({
|
||||||
ref: createRef(nodeId, p.path),
|
ref: createRef(nodeId, p.path),
|
||||||
label: displayLabel,
|
label: displayLabel,
|
||||||
|
|
@ -312,7 +322,7 @@ export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
|
||||||
if (ref) onChange(ref);
|
if (ref) onChange(ref);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<option value="">{placeholder ?? t('Datenquelle wählen…')}</option>
|
||||||
{options.map((o) => (
|
{options.map((o) => (
|
||||||
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
||||||
{o.label}
|
{o.label}
|
||||||
|
|
@ -343,13 +353,13 @@ function getFormFieldType(
|
||||||
if (!fieldName) return null;
|
if (!fieldName) return null;
|
||||||
const field = raw.find((f: unknown) => f && typeof f === 'object' && (f as Record<string, unknown>).name === fieldName);
|
const field = raw.find((f: unknown) => f && typeof f === 'object' && (f as Record<string, unknown>).name === fieldName);
|
||||||
if (!field || typeof field !== 'object') return null;
|
if (!field || typeof field !== 'object') return null;
|
||||||
const t = String((field as Record<string, unknown>).type ?? 'text').toLowerCase();
|
const rawFieldType = String((field as Record<string, unknown>).type ?? 'text').toLowerCase();
|
||||||
if (t === 'number') return 'number';
|
if (rawFieldType === 'number') return 'number';
|
||||||
if (t === 'email') return 'email';
|
if (rawFieldType === 'email') return 'email';
|
||||||
if (t === 'date' || t === 'datetime') return 'date';
|
if (rawFieldType === 'date' || rawFieldType === 'datetime') return 'date';
|
||||||
if (t === 'boolean' || t === 'checkbox') return 'boolean';
|
if (rawFieldType === 'boolean' || rawFieldType === 'checkbox') return 'boolean';
|
||||||
if (t === 'clickup_tasks') return 'string';
|
if (rawFieldType === 'clickup_tasks') return 'string';
|
||||||
if (t === 'clickup_status') return 'string';
|
if (rawFieldType === 'clickup_status') return 'string';
|
||||||
return 'string';
|
return 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ function _parseFields(params: Record<string, unknown>, t: (key: string) => strin
|
||||||
const o = f as Record<string, unknown>;
|
const o = f as Record<string, unknown>;
|
||||||
const fieldType = String(o.type ?? 'text');
|
const fieldType = String(o.type ?? 'text');
|
||||||
const name = String(o.name ?? `field${i + 1}`);
|
const name = String(o.name ?? `field${i + 1}`);
|
||||||
const label = String(o.label ?? `Feld ${i + 1}`);
|
const label = String(o.label ?? `${t('Feld')} ${i + 1}`);
|
||||||
const type = (
|
const type = (
|
||||||
FORM_FIELD_TYPES.includes(fieldType as (typeof FORM_FIELD_TYPES)[number]) ? fieldType : 'text'
|
FORM_FIELD_TYPES.includes(fieldType as (typeof FORM_FIELD_TYPES)[number]) ? fieldType : 'text'
|
||||||
) as FormField['type'];
|
) as FormField['type'];
|
||||||
|
|
@ -39,7 +39,7 @@ function _parseFields(params: Record<string, unknown>, t: (key: string) => strin
|
||||||
}
|
}
|
||||||
return { name, label, type };
|
return { name, label, type };
|
||||||
}
|
}
|
||||||
return { name: `field${i + 1}`, label: `Feld ${i + 1}`, type: 'text' as const };
|
return { name: `field${i + 1}`, label: `${t('Feld')} ${i + 1}`, type: 'text' as const };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,8 +54,9 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
return (
|
return (
|
||||||
<div className={styles.startNodeDoc}>
|
<div className={styles.startNodeDoc}>
|
||||||
<p className={styles.startNodeDocIntro}>
|
<p className={styles.startNodeDocIntro}>
|
||||||
<strong>Formular-Felder</strong> werden beim Start ausgefüllt und liegen unter{' '}
|
<strong>{t('Formular-Felder')}</strong>{' '}
|
||||||
<code>payload.<name></code> in der Start-Ausgabe.
|
{t('werden beim Start ausgefüllt und liegen unter')}{' '}
|
||||||
|
<code>payload.<name></code> {t('in der Start-Ausgabe.')}
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.formFieldsList}>
|
<div className={styles.formFieldsList}>
|
||||||
{fields.map((f, idx) => (
|
{fields.map((f, idx) => (
|
||||||
|
|
@ -72,7 +73,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className={styles.startsInput}
|
className={styles.startsInput}
|
||||||
placeholder="Beschriftung"
|
placeholder={t('Beschriftung')}
|
||||||
value={f.label}
|
value={f.label}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = [...fields];
|
const next = [...fields];
|
||||||
|
|
@ -94,11 +95,11 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
setFields(next);
|
setFields(next);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="text">Text</option>
|
<option value="text">{t('Text')}</option>
|
||||||
<option value="number">Zahl</option>
|
<option value="number">{t('Zahl')}</option>
|
||||||
<option value="email">E-Mail</option>
|
<option value="email">{t('E-Mail')}</option>
|
||||||
<option value="date">Datum</option>
|
<option value="date">{t('Datum')}</option>
|
||||||
<option value="boolean">Ja/Nein</option>
|
<option value="boolean">{t('Ja/Nein')}</option>
|
||||||
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
|
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
|
|
@ -117,7 +118,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||||
setFields([...fields, { name: `field${fields.length + 1}`, label: t('Neues Feld'), type: 'text' }])
|
setFields([...fields, { name: `field${fields.length + 1}`, label: t('Neues Feld'), type: 'text' }])
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
+ Feld
|
{t('+ Feld')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -209,8 +209,9 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
return (
|
return (
|
||||||
<div className={styles.schedulePanel}>
|
<div className={styles.schedulePanel}>
|
||||||
<p className={styles.startNodeDocIntro}>
|
<p className={styles.startNodeDocIntro}>
|
||||||
Legen Sie fest, <strong>wann</strong> dieser Workflow automatisch laufen soll. Der technische Cron-Ausdruck wird
|
{t(
|
||||||
unten automatisch erzeugt.
|
'Legen Sie fest, wann dieser Workflow automatisch laufen soll. Der technische Cron-Ausdruck wird unten automatisch erzeugt.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<LayoutGroup>
|
<LayoutGroup>
|
||||||
|
|
@ -255,7 +256,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
<div className={styles.scheduleModeConfig}>
|
<div className={styles.scheduleModeConfig}>
|
||||||
{o.value === 'daily' && (
|
{o.value === 'daily' && (
|
||||||
<label className={styles.scheduleFieldRow}>
|
<label className={styles.scheduleFieldRow}>
|
||||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={60}
|
step={60}
|
||||||
|
|
@ -268,7 +269,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
|
|
||||||
{o.value === 'weekdays' && (
|
{o.value === 'weekdays' && (
|
||||||
<label className={styles.scheduleFieldRow}>
|
<label className={styles.scheduleFieldRow}>
|
||||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={60}
|
step={60}
|
||||||
|
|
@ -282,7 +283,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
{o.value === 'weekly' && (
|
{o.value === 'weekly' && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.scheduleFieldCol}>
|
<div className={styles.scheduleFieldCol}>
|
||||||
<span className={styles.scheduleFieldLabel}>Wochentage</span>
|
<span className={styles.scheduleFieldLabel}>{t('Wochentage')}</span>
|
||||||
<div className={styles.scheduleWeekdayToggles}>
|
<div className={styles.scheduleWeekdayToggles}>
|
||||||
{WEEKDAYS_MO_SO.map(({ cronDow, label }) => (
|
{WEEKDAYS_MO_SO.map(({ cronDow, label }) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -293,13 +294,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
}
|
}
|
||||||
onClick={() => toggleWeekday(cronDow)}
|
onClick={() => toggleWeekday(cronDow)}
|
||||||
>
|
>
|
||||||
{label}
|
{t(label)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label className={styles.scheduleFieldRow}>
|
<label className={styles.scheduleFieldRow}>
|
||||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={60}
|
step={60}
|
||||||
|
|
@ -323,7 +324,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
}
|
}
|
||||||
onClick={() => setCalendarPeriod('monthly')}
|
onClick={() => setCalendarPeriod('monthly')}
|
||||||
>
|
>
|
||||||
Monatlich
|
{t('Monatlich')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -334,13 +335,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
}
|
}
|
||||||
onClick={() => setCalendarPeriod('yearly')}
|
onClick={() => setCalendarPeriod('yearly')}
|
||||||
>
|
>
|
||||||
Jährlich
|
{t('Jährlich')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{spec.calendarPeriod === 'monthly' && (
|
{spec.calendarPeriod === 'monthly' && (
|
||||||
<label className={styles.scheduleFieldRow}>
|
<label className={styles.scheduleFieldRow}>
|
||||||
<span className={styles.scheduleFieldLabel}>Monatstag</span>
|
<span className={styles.scheduleFieldLabel}>{t('Monatstag')}</span>
|
||||||
<select
|
<select
|
||||||
className={styles.scheduleSelect}
|
className={styles.scheduleSelect}
|
||||||
value={spec.monthDay}
|
value={spec.monthDay}
|
||||||
|
|
@ -358,7 +359,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
{spec.calendarPeriod === 'yearly' && (
|
{spec.calendarPeriod === 'yearly' && (
|
||||||
<div className={styles.scheduleYearlyRow}>
|
<div className={styles.scheduleYearlyRow}>
|
||||||
<label className={styles.scheduleFieldRowGrow}>
|
<label className={styles.scheduleFieldRowGrow}>
|
||||||
<span className={styles.scheduleFieldLabel}>Monat</span>
|
<span className={styles.scheduleFieldLabel}>{t('Monat')}</span>
|
||||||
<select
|
<select
|
||||||
className={styles.scheduleSelect}
|
className={styles.scheduleSelect}
|
||||||
value={spec.monthIndex}
|
value={spec.monthIndex}
|
||||||
|
|
@ -366,13 +367,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
>
|
>
|
||||||
{MONTH_NAMES_DE.map((name, i) => (
|
{MONTH_NAMES_DE.map((name, i) => (
|
||||||
<option key={i + 1} value={i + 1}>
|
<option key={i + 1} value={i + 1}>
|
||||||
{name}
|
{t(name)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className={styles.scheduleFieldRowGrow}>
|
<label className={styles.scheduleFieldRowGrow}>
|
||||||
<span className={styles.scheduleFieldLabel}>Tag</span>
|
<span className={styles.scheduleFieldLabel}>{t('Tag')}</span>
|
||||||
<select
|
<select
|
||||||
className={styles.scheduleSelect}
|
className={styles.scheduleSelect}
|
||||||
value={spec.monthDay}
|
value={spec.monthDay}
|
||||||
|
|
@ -389,7 +390,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label className={styles.scheduleFieldRow}>
|
<label className={styles.scheduleFieldRow}>
|
||||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={60}
|
step={60}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { NodeConfigRendererProps } from '../shared/types';
|
import type { NodeConfigRendererProps } from '../shared/types';
|
||||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||||
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
const SCHEMA_EXAMPLE = `{
|
const SCHEMA_EXAMPLE = `{
|
||||||
"trigger": {
|
"trigger": {
|
||||||
|
|
@ -22,17 +23,20 @@ const SCHEMA_EXAMPLE = `{
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
export const StartNodeConfig: React.FC<NodeConfigRendererProps> = () => {
|
export const StartNodeConfig: React.FC<NodeConfigRendererProps> = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
return (
|
return (
|
||||||
<div className={styles.startNodeDoc}>
|
<div className={styles.startNodeDoc}>
|
||||||
<p className={styles.startNodeDocIntro}>
|
<p className={styles.startNodeDocIntro}>
|
||||||
Die <strong>Start</strong>-Node liefert beim Ausführen immer dieselbe Struktur. Den <strong>Einstiegstyp</strong>{' '}
|
{t(
|
||||||
(manuell, Formular, Zeitplan, …) wählen Sie über das <strong>Zahnrad</strong> oben in der
|
'Die Start-Node liefert beim Ausführen immer dieselbe Struktur. Den Einstiegstyp (manuell, Formular, Zeitplan, …) wählen Sie über das Zahnrad oben in der Workflow-Konfiguration.'
|
||||||
Workflow-Konfiguration.
|
)}
|
||||||
|
</p>
|
||||||
|
<p className={styles.startNodeDocSub}>
|
||||||
|
{t('Nachgelagerte Nodes können z. B. auf')}{' '}
|
||||||
|
<code>payload</code> {t('und')} <code>trigger.type</code> {t('zugreifen.')}
|
||||||
</p>
|
</p>
|
||||||
<p className={styles.startNodeDocSub}>Nachgelagerte Nodes können z. B. auf <code>payload</code> und{' '}
|
|
||||||
<code>trigger.type</code> zugreifen.</p>
|
|
||||||
<div className={styles.startNodeSchema}>
|
<div className={styles.startNodeSchema}>
|
||||||
<div className={styles.startNodeSchemaTitle}>Ausgabe-Schema</div>
|
<div className={styles.startNodeSchemaTitle}>{t('Ausgabe-Schema')}</div>
|
||||||
<pre className={styles.startNodePre}>{SCHEMA_EXAMPLE}</pre>
|
<pre className={styles.startNodePre}>{SCHEMA_EXAMPLE}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
||||||
<option value="">{t('MIME-Typ wählen')}</option>
|
<option value="">{t('MIME-Typ wählen')}</option>
|
||||||
{mimeTypeOptions.map((o) => (
|
{mimeTypeOptions.map((o) => (
|
||||||
<option key={o.value} value={o.value}>
|
<option key={o.value} value={o.value}>
|
||||||
{o.label} ({o.value})
|
{t(o.label)} ({o.value})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -166,7 +166,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
||||||
className={styles.startsInput}
|
className={styles.startsInput}
|
||||||
value={valStr}
|
value={valStr}
|
||||||
onChange={(e) => handleCaseValueChange(index, e.target.value)}
|
onChange={(e) => handleCaseValueChange(index, e.target.value)}
|
||||||
placeholder={isMimeTypeRef ? 'z.B. application/pdf' : `Wert`}
|
placeholder={isMimeTypeRef ? t('z.B. application/pdf') : t('Wert')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -185,7 +185,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
||||||
return (
|
return (
|
||||||
<div className={styles.ifElseConditionEditor}>
|
<div className={styles.ifElseConditionEditor}>
|
||||||
<div className={styles.ifElseConditionRow}>
|
<div className={styles.ifElseConditionRow}>
|
||||||
<label>Datenquelle</label>
|
<label>{t('Datenquelle')}</label>
|
||||||
<RefSourceSelect
|
<RefSourceSelect
|
||||||
value={ref}
|
value={ref}
|
||||||
onChange={handleRefChange}
|
onChange={handleRefChange}
|
||||||
|
|
@ -221,7 +221,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
||||||
>
|
>
|
||||||
{operators.map((o) => (
|
{operators.map((o) => (
|
||||||
<option key={o.value} value={o.value}>
|
<option key={o.value} value={o.value}>
|
||||||
{o.label}
|
{t(o.label)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -241,7 +241,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<button type="button" className={styles.startsAddBtn} onClick={addCase}>
|
<button type="button" className={styles.startsAddBtn} onClick={addCase}>
|
||||||
+ Fall
|
{t('+ Fall')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -162,13 +162,6 @@ const _SCOPE_ICONS: Record<string, string> = {
|
||||||
|
|
||||||
const _SCOPE_CYCLE: string[] = ['personal', 'featureInstance', 'mandate'];
|
const _SCOPE_CYCLE: string[] = ['personal', 'featureInstance', 'mandate'];
|
||||||
|
|
||||||
const _SCOPE_LABELS: Record<string, string> = {
|
|
||||||
personal: 'Persönlich',
|
|
||||||
featureInstance: 'Instanz',
|
|
||||||
mandate: 'Mandant',
|
|
||||||
global: 'Global',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SelectionCtx {
|
interface SelectionCtx {
|
||||||
selectedItemIds: Set<string>;
|
selectedItemIds: Set<string>;
|
||||||
selectedFileIds: string[];
|
selectedFileIds: string[];
|
||||||
|
|
@ -187,6 +180,12 @@ interface SelectionCtx {
|
||||||
|
|
||||||
function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const scopeLabels = useMemo((): Record<string, string> => ({
|
||||||
|
personal: t('Persönlich'),
|
||||||
|
featureInstance: t('Instanz'),
|
||||||
|
mandate: t('Mandant'),
|
||||||
|
global: t('Global'),
|
||||||
|
}), [t]);
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
const [renaming, setRenaming] = useState(false);
|
const [renaming, setRenaming] = useState(false);
|
||||||
const [renameValue, setRenameValue] = useState('');
|
const [renameValue, setRenameValue] = useState('');
|
||||||
|
|
@ -257,14 +256,14 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
||||||
<span className={styles.rightZone}>
|
<span className={styles.rightZone}>
|
||||||
<span className={styles.actions}>
|
<span className={styles.actions}>
|
||||||
{sel.onRenameFile && !multiSelected && (
|
{sel.onRenameFile && !multiSelected && (
|
||||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(file.fileName); setRenaming(true); }} title="Umbenennen">
|
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(file.fileName); setRenaming(true); }} title={t('Umbenennen')}>
|
||||||
<FaPen />
|
<FaPen />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{multiSelected && isSelected ? (
|
{multiSelected && isSelected ? (
|
||||||
<>
|
<>
|
||||||
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && (
|
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && (
|
||||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={`${sel.selectedFolderIds.length} Ordner löschen`}>
|
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={t('{count} Ordner löschen', { count: String(sel.selectedFolderIds.length) })}>
|
||||||
<FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
|
<FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
|
||||||
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
|
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -300,7 +299,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
||||||
const next = _SCOPE_CYCLE[(idx + 1) % _SCOPE_CYCLE.length];
|
const next = _SCOPE_CYCLE[(idx + 1) % _SCOPE_CYCLE.length];
|
||||||
sel.onScopeChange(file.id, next);
|
sel.onScopeChange(file.id, next);
|
||||||
}}
|
}}
|
||||||
title={`Scope: ${_SCOPE_LABELS[file.scope!] || file.scope} (klicken zum Wechseln)`}
|
title={`${t('Scope')}: ${scopeLabels[file.scope!] || file.scope} (${t('klicken zum Wechseln')})`}
|
||||||
style={{ fontSize: 14 }}
|
style={{ fontSize: 14 }}
|
||||||
>
|
>
|
||||||
{_SCOPE_ICONS[file.scope!] || '\uD83D\uDC64'}
|
{_SCOPE_ICONS[file.scope!] || '\uD83D\uDC64'}
|
||||||
|
|
@ -381,12 +380,12 @@ function _TreeNode({
|
||||||
const _handleAdd = useCallback(async (e: React.MouseEvent) => {
|
const _handleAdd = useCallback(async (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!onCreateFolder) return;
|
if (!onCreateFolder) return;
|
||||||
const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
|
const name = await promptFolderName(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
|
||||||
if (name?.trim()) {
|
if (name?.trim()) {
|
||||||
await onCreateFolder(name.trim(), node.id);
|
await onCreateFolder(name.trim(), node.id);
|
||||||
if (!expandedIds.has(node.id)) onToggle(node.id);
|
if (!expandedIds.has(node.id)) onToggle(node.id);
|
||||||
}
|
}
|
||||||
}, [onCreateFolder, node.id, expandedIds, onToggle, promptFolderName]);
|
}, [onCreateFolder, node.id, expandedIds, onToggle, promptFolderName, t]);
|
||||||
|
|
||||||
const _handleDeleteSingle = useCallback(async (e: React.MouseEvent) => {
|
const _handleDeleteSingle = useCallback(async (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -753,7 +752,7 @@ export default function FolderTree({
|
||||||
onDrop={_handleRootDrop}
|
onDrop={_handleRootDrop}
|
||||||
>
|
>
|
||||||
<span className={styles.folderIcon}><FaGlobe /></span>
|
<span className={styles.folderIcon}><FaGlobe /></span>
|
||||||
<span className={`${styles.folderName} ${styles.rootLabel}`}>(Global)</span>
|
<span className={`${styles.folderName} ${styles.rootLabel}`}>({t('Global')})</span>
|
||||||
<span className={styles.rootActions}>
|
<span className={styles.rootActions}>
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}>
|
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}>
|
||||||
|
|
@ -765,7 +764,7 @@ export default function FolderTree({
|
||||||
className={styles.actionBtn}
|
className={styles.actionBtn}
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
|
const name = await promptFolderName(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
|
||||||
if (name?.trim()) await onCreateFolder(name.trim(), null);
|
if (name?.trim()) await onCreateFolder(name.trim(), null);
|
||||||
}}
|
}}
|
||||||
title={t('Neuer Ordner')}
|
title={t('Neuer Ordner')}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import { FaFolder, FaFolderOpen, FaChevronRight, FaGlobe } from 'react-icons/fa';
|
import { FaFolder, FaFolderOpen, FaChevronRight, FaGlobe } from 'react-icons/fa';
|
||||||
import styles from './FolderTree.module.css';
|
import styles from './FolderTree.module.css';
|
||||||
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export interface BrowseEntry {
|
export interface BrowseEntry {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -100,6 +101,7 @@ function _FolderRow({
|
||||||
onSelectFolder?: (path: string) => void;
|
onSelectFolder?: (path: string) => void;
|
||||||
foldersOnly: boolean;
|
foldersOnly: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const isExpanded = expandedPaths.has(entry.path);
|
const isExpanded = expandedPaths.has(entry.path);
|
||||||
const isSelected = selectedPath === entry.path;
|
const isSelected = selectedPath === entry.path;
|
||||||
const children = loadedChildren[entry.path] ?? [];
|
const children = loadedChildren[entry.path] ?? [];
|
||||||
|
|
@ -132,7 +134,7 @@ function _FolderRow({
|
||||||
<span
|
<span
|
||||||
className={`${styles.chevron} ${isExpanded ? styles.expanded : ''}`}
|
className={`${styles.chevron} ${isExpanded ? styles.expanded : ''}`}
|
||||||
onClick={handleChevronClick}
|
onClick={handleChevronClick}
|
||||||
title={isExpanded ? 'Einklappen' : 'Erweitern'}
|
title={isExpanded ? t('Einklappen') : t('Erweitern')}
|
||||||
>
|
>
|
||||||
<FaChevronRight />
|
<FaChevronRight />
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -148,7 +150,7 @@ function _FolderRow({
|
||||||
<div className={styles.children}>
|
<div className={styles.children}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
|
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
|
||||||
Wird geladen…
|
{t('Wird geladen…')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -177,7 +179,7 @@ function _FolderRow({
|
||||||
))}
|
))}
|
||||||
{children.length === 0 && (
|
{children.length === 0 && (
|
||||||
<div style={{ padding: '0.4rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
|
<div style={{ padding: '0.4rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
|
||||||
Leer
|
{t('Leer')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -199,6 +201,7 @@ export function SharepointBrowseTree({
|
||||||
selectedPath,
|
selectedPath,
|
||||||
initialChildren = [],
|
initialChildren = [],
|
||||||
}: SharepointBrowseTreeProps) {
|
}: SharepointBrowseTreeProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set([rootPath]));
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set([rootPath]));
|
||||||
const [loadedChildren, setLoadedChildren] = useState<Record<string, BrowseEntry[]>>(() =>
|
const [loadedChildren, setLoadedChildren] = useState<Record<string, BrowseEntry[]>>(() =>
|
||||||
initialChildren.length > 0 ? { [rootPath]: initialChildren } : {}
|
initialChildren.length > 0 ? { [rootPath]: initialChildren } : {}
|
||||||
|
|
@ -261,11 +264,12 @@ export function SharepointBrowseTree({
|
||||||
<span
|
<span
|
||||||
className={`${styles.chevron} ${isRootExpanded ? styles.expanded : ''}`}
|
className={`${styles.chevron} ${isRootExpanded ? styles.expanded : ''}`}
|
||||||
onClick={() => handleToggle(rootPath)}
|
onClick={() => handleToggle(rootPath)}
|
||||||
|
title={isRootExpanded ? t('Einklappen') : t('Erweitern')}
|
||||||
>
|
>
|
||||||
<FaChevronRight />
|
<FaChevronRight />
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.folderIcon}><FaGlobe /></span>
|
<span className={styles.folderIcon}><FaGlobe /></span>
|
||||||
<span className={`${styles.folderName} ${styles.rootLabel}`}>SharePoint</span>
|
<span className={`${styles.folderName} ${styles.rootLabel}`}>{t('SharePoint')}</span>
|
||||||
{rootLoading && (
|
{rootLoading && (
|
||||||
<span style={{ fontSize: 10, color: 'var(--color-text-secondary,#999)', marginLeft: 4 }}>…</span>
|
<span style={{ fontSize: 10, color: 'var(--color-text-secondary,#999)', marginLeft: 4 }}>…</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -274,7 +278,7 @@ export function SharepointBrowseTree({
|
||||||
<div className={styles.children}>
|
<div className={styles.children}>
|
||||||
{rootLoading ? (
|
{rootLoading ? (
|
||||||
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
|
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
|
||||||
Sites werden geladen…
|
{t('Sites werden geladen…')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -303,7 +307,7 @@ export function SharepointBrowseTree({
|
||||||
))}
|
))}
|
||||||
{rootItems.length === 0 && !rootLoading && (
|
{rootItems.length === 0 && !rootLoading && (
|
||||||
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
|
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
|
||||||
Keine Einträge
|
{t('Keine Einträge')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -131,11 +131,11 @@ export function DeleteActionButton<T = any>({
|
||||||
} else {
|
} else {
|
||||||
// Refetch to restore the item in case of failure
|
// Refetch to restore the item in case of failure
|
||||||
await refetch();
|
await refetch();
|
||||||
onError?.(row, 'Delete failed');
|
onError?.(row, t('Löschen fehlgeschlagen'));
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Delete failed:', error);
|
console.error('Delete failed:', error);
|
||||||
onError?.(row, error.message || 'Delete failed');
|
onError?.(row, error.message || t('Löschen fehlgeschlagen'));
|
||||||
// Refetch to restore the item in case of failure
|
// Refetch to restore the item in case of failure
|
||||||
await refetch();
|
await refetch();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ export function ViewActionButton<T = any>({
|
||||||
isOpen={isPopupOpen}
|
isOpen={isPopupOpen}
|
||||||
onClose={() => setIsPopupOpen(false)}
|
onClose={() => setIsPopupOpen(false)}
|
||||||
fileId={(row as any)[idField]}
|
fileId={(row as any)[idField]}
|
||||||
fileName={(row as any)[nameField] || 'Unknown Item'}
|
fileName={(row as any)[nameField] || t('Unbekanntes Element')}
|
||||||
mimeType={mimeType}
|
mimeType={mimeType}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -115,7 +115,7 @@ export function ViewActionButton<T = any>({
|
||||||
>
|
>
|
||||||
<div style={{ padding: '20px' }}>
|
<div style={{ padding: '20px' }}>
|
||||||
<h3 style={{ marginBottom: '20px', fontSize: '1.2rem', fontWeight: 'bold' }}>
|
<h3 style={{ marginBottom: '20px', fontSize: '1.2rem', fontWeight: 'bold' }}>
|
||||||
{(row as any)[nameField] || (row as any)[idField] || 'Details'}
|
{(row as any)[nameField] || (row as any)[idField] || t('Details')}
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{ display: 'grid', gap: '15px' }}>
|
<div style={{ display: 'grid', gap: '15px' }}>
|
||||||
{Object.entries(row as Record<string, any>)
|
{Object.entries(row as Record<string, any>)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const isTextMultilingual = (value: any): boolean => {
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
|
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return 'en' in value && typeof value.en === 'string';
|
return 'xx' in value && typeof value.xx === 'string';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Note: Field types are determined ONLY by the explicit 'type' property.
|
// Note: Field types are determined ONLY by the explicit 'type' property.
|
||||||
|
|
@ -50,7 +50,7 @@ export interface AttributeDefinition {
|
||||||
|
|
||||||
export interface AttributeOption {
|
export interface AttributeOption {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string | { [language: string]: string };
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormGeneratorForm props
|
// FormGeneratorForm props
|
||||||
|
|
@ -243,7 +243,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
if (!isTextMultilingual(processedData[attr.name])) {
|
if (!isTextMultilingual(processedData[attr.name])) {
|
||||||
// If it's a string, convert to TextMultilingual
|
// If it's a string, convert to TextMultilingual
|
||||||
if (typeof processedData[attr.name] === 'string') {
|
if (typeof processedData[attr.name] === 'string') {
|
||||||
processedData[attr.name] = { en: processedData[attr.name] };
|
processedData[attr.name] = { xx: processedData[attr.name] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -268,8 +268,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
if (attr.default !== undefined) {
|
if (attr.default !== undefined) {
|
||||||
initialData[attr.name] = attr.default;
|
initialData[attr.name] = attr.default;
|
||||||
} else if (isMultilingual(attr)) {
|
} else if (isMultilingual(attr)) {
|
||||||
// Initialize TextMultilingual fields with empty object
|
initialData[attr.name] = { xx: '' };
|
||||||
initialData[attr.name] = { en: '' };
|
|
||||||
} else {
|
} else {
|
||||||
initialData[attr.name] = getDefaultValueForType(attr.type);
|
initialData[attr.name] = getDefaultValueForType(attr.type);
|
||||||
}
|
}
|
||||||
|
|
@ -327,11 +326,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
if (typeof opt === 'string' || typeof opt === 'number') {
|
if (typeof opt === 'string' || typeof opt === 'number') {
|
||||||
return { value: opt, label: String(opt) };
|
return { value: opt, label: String(opt) };
|
||||||
}
|
}
|
||||||
// Handle multilingual labels
|
return { value: opt.value, label: opt.label || String(opt.value) };
|
||||||
const labelValue = typeof opt.label === 'string'
|
|
||||||
? opt.label
|
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
|
||||||
return { value: opt.value, label: labelValue };
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,12 +431,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
return { value: opt, label: opt };
|
return { value: opt, label: opt };
|
||||||
}
|
}
|
||||||
if (typeof opt === 'object' && 'value' in opt) {
|
if (typeof opt === 'object' && 'value' in opt) {
|
||||||
const labelValue = typeof opt.label === 'string'
|
|
||||||
? opt.label
|
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
|
||||||
return {
|
return {
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: labelValue
|
label: opt.label || String(opt.value)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { value: String(opt), label: String(opt) };
|
return { value: String(opt), label: String(opt) };
|
||||||
|
|
@ -464,9 +456,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
// Special handling for TextMultilingual fields (by explicit type only)
|
// Special handling for TextMultilingual fields (by explicit type only)
|
||||||
const isMultilingualField = isMultilingualType(attr.type as AttributeType);
|
const isMultilingualField = isMultilingualType(attr.type as AttributeType);
|
||||||
if (isMultilingualField && isTextMultilingual(value)) {
|
if (isMultilingualField && isTextMultilingual(value)) {
|
||||||
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
if (!value.xx || typeof value.xx !== 'string' || value.xx.trim() === '') {
|
||||||
newErrors[attr.name] = t('{fieldLabel} ist erforderlich', {
|
newErrors[attr.name] = t('{fieldLabel} ist erforderlich', {
|
||||||
fieldLabel: `${attr.label} (Englisch)`,
|
fieldLabel: attr.label,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -655,21 +647,17 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build multilingual language list dynamically from availableLanguages.
|
// xx = source/default text (required), then all available languages dynamically.
|
||||||
// 'en' is always first and required; remaining languages follow in DB order.
|
|
||||||
const multilingualLangs = useMemo(() => {
|
const multilingualLangs = useMemo(() => {
|
||||||
const base: { code: string; uiLabel: string; required: boolean }[] = [
|
const langs: { code: string; uiLabel: string; required: boolean }[] = [
|
||||||
{ code: 'en', uiLabel: 'EN', required: true },
|
{ code: 'xx', uiLabel: t('Quelltext'), required: true },
|
||||||
];
|
];
|
||||||
for (const lang of availableLanguages) {
|
for (const lang of availableLanguages) {
|
||||||
if (lang.code === 'en' || lang.code === 'xx') continue;
|
if (lang.code === 'xx') continue;
|
||||||
base.push({ code: lang.code, uiLabel: lang.code.toUpperCase(), required: false });
|
langs.push({ code: lang.code, uiLabel: lang.code.toUpperCase(), required: false });
|
||||||
}
|
}
|
||||||
if (base.length === 1) {
|
return langs;
|
||||||
base.push({ code: 'de', uiLabel: 'DE', required: false });
|
}, [availableLanguages, t]);
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}, [availableLanguages]);
|
|
||||||
|
|
||||||
const _handleAutoTranslate = async (attrName: string, multilingualValue: Record<string, string>) => {
|
const _handleAutoTranslate = async (attrName: string, multilingualValue: Record<string, string>) => {
|
||||||
const sourceLang = multilingualLangs.find(l => (multilingualValue[l.code] || '').trim())?.code;
|
const sourceLang = multilingualLangs.find(l => (multilingualValue[l.code] || '').trim())?.code;
|
||||||
|
|
@ -702,11 +690,11 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
|
|
||||||
// Render multilingual field
|
// Render multilingual field
|
||||||
const renderMultilingualField = (attr: AttributeDefinition) => {
|
const renderMultilingualField = (attr: AttributeDefinition) => {
|
||||||
const value = formData[attr.name] || { en: '' };
|
const value = formData[attr.name] || { xx: '' };
|
||||||
const hasError = errors[attr.name];
|
const hasError = errors[attr.name];
|
||||||
const isReadonly = mode === 'display' || attr.readonly || !isFieldEditableInMode(attr, mode);
|
const isReadonly = mode === 'display' || attr.readonly || !isFieldEditableInMode(attr, mode);
|
||||||
|
|
||||||
const multilingualValue = isTextMultilingual(value) ? value : { en: typeof value === 'string' ? value : '' };
|
const multilingualValue = isTextMultilingual(value) ? value : { xx: typeof value === 'string' ? value : '' };
|
||||||
|
|
||||||
const handleMultilingualChange = (langCode: string, langValue: string) => {
|
const handleMultilingualChange = (langCode: string, langValue: string) => {
|
||||||
const newValue = { ...multilingualValue, [langCode]: langValue };
|
const newValue = { ...multilingualValue, [langCode]: langValue };
|
||||||
|
|
@ -745,7 +733,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
onChange={(e) => handleMultilingualChange(lang.code, e.target.value)}
|
onChange={(e) => handleMultilingualChange(lang.code, e.target.value)}
|
||||||
onFocus={() => handleFieldFocus(`${attr.name}.${lang.code}`, true)}
|
onFocus={() => handleFieldFocus(`${attr.name}.${lang.code}`, true)}
|
||||||
onBlur={() => handleFieldFocus(`${attr.name}.${lang.code}`, false)}
|
onBlur={() => handleFieldFocus(`${attr.name}.${lang.code}`, false)}
|
||||||
className={`${styles.fieldInput} ${hasError && lang.code === 'en' ? styles.fieldError : ''}`}
|
className={`${styles.fieldInput} ${hasError && lang.code === 'xx' ? styles.fieldError : ''}`}
|
||||||
/>
|
/>
|
||||||
<label className={getLabelClass(`${attr.name}.${lang.code}`, multilingualValue[lang.code])}>
|
<label className={getLabelClass(`${attr.name}.${lang.code}`, multilingualValue[lang.code])}>
|
||||||
{lang.uiLabel}
|
{lang.uiLabel}
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,6 @@ const CHART_COLORS = [
|
||||||
'#bab0ac'
|
'#bab0ac'
|
||||||
];
|
];
|
||||||
|
|
||||||
const MONTH_LABELS: Record<string, string> = {
|
|
||||||
'01': 'Jan', '02': 'Feb', '03': 'Mär', '04': 'Apr',
|
|
||||||
'05': 'Mai', '06': 'Jun', '07': 'Jul', '08': 'Aug',
|
|
||||||
'09': 'Sep', '10': 'Okt', '11': 'Nov', '12': 'Dez'
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// HELPER FUNCTIONS
|
// HELPER FUNCTIONS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -54,15 +48,22 @@ function _defaultFormatCurrency(value: number, currencyCode: string): string {
|
||||||
return `${currencyCode} ${value.toFixed(2)}`;
|
return `${currencyCode} ${value.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _formatDateLabel(dateStr: string): string {
|
function _createFormatDateLabel(t: (key: string) => string): (dateStr: string) => string {
|
||||||
const parts = dateStr.split('-');
|
const monthLabels: Record<string, string> = {
|
||||||
if (parts.length === 3) {
|
'01': t('Jan'), '02': t('Feb'), '03': t('Mär'), '04': t('Apr'),
|
||||||
return `${parseInt(parts[2], 10)}.`;
|
'05': t('Mai'), '06': t('Jun'), '07': t('Jul'), '08': t('Aug'),
|
||||||
}
|
'09': t('Sep'), '10': t('Okt'), '11': t('Nov'), '12': t('Dez'),
|
||||||
if (parts.length === 2) {
|
};
|
||||||
return MONTH_LABELS[parts[1]] || parts[1];
|
return (dateStr: string) => {
|
||||||
}
|
const parts = dateStr.split('-');
|
||||||
return dateStr;
|
if (parts.length === 3) {
|
||||||
|
return `${parseInt(parts[2], 10)}.`;
|
||||||
|
}
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return monthLabels[parts[1]] || parts[1];
|
||||||
|
}
|
||||||
|
return dateStr;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -74,12 +75,13 @@ interface CustomTooltipProps {
|
||||||
payload?: any[];
|
payload?: any[];
|
||||||
label?: string;
|
label?: string;
|
||||||
formatValue?: (value: number) => string;
|
formatValue?: (value: number) => string;
|
||||||
|
formatDateLabel: (dateStr: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _CustomTooltip: React.FC<CustomTooltipProps> = ({ active, payload, label, formatValue }) => {
|
const _CustomTooltip: React.FC<CustomTooltipProps> = ({ active, payload, label, formatValue, formatDateLabel }) => {
|
||||||
if (!active || !payload?.length) return null;
|
if (!active || !payload?.length) return null;
|
||||||
|
|
||||||
const displayLabel = label ? _formatDateLabel(String(label)) : '';
|
const displayLabel = label ? formatDateLabel(String(label)) : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.customTooltip}>
|
<div className={styles.customTooltip}>
|
||||||
|
|
@ -115,15 +117,18 @@ const _renderKpiGrid = (section: ReportSectionKpi): React.ReactNode => {
|
||||||
|
|
||||||
// --- Bar Chart (vertical) ---
|
// --- Bar Chart (vertical) ---
|
||||||
|
|
||||||
const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string): React.ReactNode => {
|
const _renderBarChart = (
|
||||||
const { t } = useLanguage();
|
section: ReportSectionBarChart,
|
||||||
|
currencyCode: string,
|
||||||
|
formatDateLabel: (dateStr: string) => string,
|
||||||
|
t: (key: string) => string,
|
||||||
|
): React.ReactNode => {
|
||||||
if (!section.data?.length) {
|
if (!section.data?.length) {
|
||||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = section.data.map(d => ({
|
const chartData = section.data.map(d => ({
|
||||||
name: _formatDateLabel(d.key),
|
name: formatDateLabel(d.key),
|
||||||
value: d.value,
|
value: d.value,
|
||||||
rawKey: d.key
|
rawKey: d.key
|
||||||
}));
|
}));
|
||||||
|
|
@ -146,12 +151,12 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
|
||||||
tickFormatter={(v) => formatter(v)}
|
tickFormatter={(v) => formatter(v)}
|
||||||
width={70}
|
width={70}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<_CustomTooltip formatValue={formatter} />} />
|
<Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
fill={section.color || CHART_COLORS[0]}
|
fill={section.color || CHART_COLORS[0]}
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
name="Wert"
|
name={t('Wert')}
|
||||||
/>
|
/>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|
@ -161,9 +166,7 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
|
||||||
|
|
||||||
// --- Horizontal Bar Chart ---
|
// --- Horizontal Bar Chart ---
|
||||||
|
|
||||||
const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode: string): React.ReactNode => {
|
const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode: string, t: (key: string) => string): React.ReactNode => {
|
||||||
const { t } = useLanguage();
|
|
||||||
|
|
||||||
if (!section.data?.length) {
|
if (!section.data?.length) {
|
||||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -194,9 +197,12 @@ const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode:
|
||||||
|
|
||||||
// --- Line Chart ---
|
// --- Line Chart ---
|
||||||
|
|
||||||
const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string): React.ReactNode => {
|
const _renderLineChart = (
|
||||||
const { t } = useLanguage();
|
section: ReportSectionLineChart,
|
||||||
|
currencyCode: string,
|
||||||
|
formatDateLabel: (dateStr: string) => string,
|
||||||
|
t: (key: string) => string,
|
||||||
|
): React.ReactNode => {
|
||||||
if (!section.data?.length) {
|
if (!section.data?.length) {
|
||||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -212,7 +218,7 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||||
axisLine={{ stroke: 'var(--border-color, #333)' }}
|
axisLine={{ stroke: 'var(--border-color, #333)' }}
|
||||||
tickFormatter={_formatDateLabel}
|
tickFormatter={formatDateLabel}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||||
|
|
@ -220,7 +226,7 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
|
||||||
tickFormatter={formatter}
|
tickFormatter={formatter}
|
||||||
width={70}
|
width={70}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<_CustomTooltip formatValue={formatter} />} />
|
<Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
|
||||||
{section.series.map((s, i) => (
|
{section.series.map((s, i) => (
|
||||||
<Line
|
<Line
|
||||||
key={s.key}
|
key={s.key}
|
||||||
|
|
@ -242,9 +248,12 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
|
||||||
|
|
||||||
// --- Area Chart ---
|
// --- Area Chart ---
|
||||||
|
|
||||||
const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string): React.ReactNode => {
|
const _renderAreaChart = (
|
||||||
const { t } = useLanguage();
|
section: ReportSectionAreaChart,
|
||||||
|
currencyCode: string,
|
||||||
|
formatDateLabel: (dateStr: string) => string,
|
||||||
|
t: (key: string) => string,
|
||||||
|
): React.ReactNode => {
|
||||||
if (!section.data?.length) {
|
if (!section.data?.length) {
|
||||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +269,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||||
axisLine={{ stroke: 'var(--border-color, #333)' }}
|
axisLine={{ stroke: 'var(--border-color, #333)' }}
|
||||||
tickFormatter={_formatDateLabel}
|
tickFormatter={formatDateLabel}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||||
|
|
@ -268,7 +277,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
|
||||||
tickFormatter={formatter}
|
tickFormatter={formatter}
|
||||||
width={70}
|
width={70}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<_CustomTooltip formatValue={formatter} />} />
|
<Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
|
||||||
{section.series.map((s, i) => (
|
{section.series.map((s, i) => (
|
||||||
<Area
|
<Area
|
||||||
key={s.key}
|
key={s.key}
|
||||||
|
|
@ -290,9 +299,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
|
||||||
|
|
||||||
// --- Pie Chart ---
|
// --- Pie Chart ---
|
||||||
|
|
||||||
const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string): React.ReactNode => {
|
const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string, t: (key: string) => string): React.ReactNode => {
|
||||||
const { t } = useLanguage();
|
|
||||||
|
|
||||||
if (!section.data?.length) {
|
if (!section.data?.length) {
|
||||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +425,7 @@ const _ReportTableSection: React.FC<ReportTableSectionProps> = ({ section, curre
|
||||||
{hasMore && !showAll && (
|
{hasMore && !showAll && (
|
||||||
<div className={styles.showMoreRow}>
|
<div className={styles.showMoreRow}>
|
||||||
<button className={styles.showMoreButton} onClick={() => setShowAll(true)}>
|
<button className={styles.showMoreButton} onClick={() => setShowAll(true)}>
|
||||||
Alle {section.rows.length} Einträge anzeigen
|
{t('Alle {count} Einträge anzeigen', { count: String(section.rows.length) })}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -436,6 +443,9 @@ interface SectionWrapperProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode }) => {
|
const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode }) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const formatDateLabel = useMemo(() => _createFormatDateLabel(t), [t]);
|
||||||
|
|
||||||
const spanClass = section.type === 'kpiGrid' || section.span === 'full'
|
const spanClass = section.type === 'kpiGrid' || section.span === 'full'
|
||||||
? styles.sectionFull
|
? styles.sectionFull
|
||||||
: section.span === 'half'
|
: section.span === 'half'
|
||||||
|
|
@ -452,20 +462,18 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _renderContent = (): React.ReactNode => {
|
const renderContent = (): React.ReactNode => {
|
||||||
const { t } = useLanguage();
|
|
||||||
|
|
||||||
switch (section.type) {
|
switch (section.type) {
|
||||||
case 'barChart':
|
case 'barChart':
|
||||||
return _renderBarChart(section, currencyCode);
|
return _renderBarChart(section, currencyCode, formatDateLabel, t);
|
||||||
case 'horizontalBar':
|
case 'horizontalBar':
|
||||||
return _renderHorizontalBar(section, currencyCode);
|
return _renderHorizontalBar(section, currencyCode, t);
|
||||||
case 'lineChart':
|
case 'lineChart':
|
||||||
return _renderLineChart(section, currencyCode);
|
return _renderLineChart(section, currencyCode, formatDateLabel, t);
|
||||||
case 'areaChart':
|
case 'areaChart':
|
||||||
return _renderAreaChart(section, currencyCode);
|
return _renderAreaChart(section, currencyCode, formatDateLabel, t);
|
||||||
case 'pieChart':
|
case 'pieChart':
|
||||||
return _renderPieChart(section, currencyCode);
|
return _renderPieChart(section, currencyCode, t);
|
||||||
case 'table':
|
case 'table':
|
||||||
return <_ReportTableSection section={section} currencyCode={currencyCode} />;
|
return <_ReportTableSection section={section} currencyCode={currencyCode} />;
|
||||||
default:
|
default:
|
||||||
|
|
@ -477,7 +485,7 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
|
||||||
<div className={`${spanClass} ${styles.sectionCard}`}>
|
<div className={`${spanClass} ${styles.sectionCard}`}>
|
||||||
{section.title && <h3 className={styles.sectionTitle}>{section.title}</h3>}
|
{section.title && <h3 className={styles.sectionTitle}>{section.title}</h3>}
|
||||||
{section.description && <p className={styles.sectionDescription}>{section.description}</p>}
|
{section.description && <p className={styles.sectionDescription}>{section.description}</p>}
|
||||||
{_renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -607,7 +615,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
|
||||||
value={filterState.dateRange?.from?.toISOString().split('T')[0] || ''}
|
value={filterState.dateRange?.from?.toISOString().split('T')[0] || ''}
|
||||||
onChange={(e) => _handleDateRangeChange('from', e.target.value)}
|
onChange={(e) => _handleDateRangeChange('from', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<span className={styles.toolbarLabel}>Bis</span>
|
<span className={styles.toolbarLabel}>{t('Bis')}</span>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className={styles.dateInput}
|
className={styles.dateInput}
|
||||||
|
|
@ -640,7 +648,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
|
||||||
value={(filterState.filters[filter.key] as string) || ''}
|
value={(filterState.filters[filter.key] as string) || ''}
|
||||||
onChange={(e) => _handleFilterChange(filter.key, e.target.value)}
|
onChange={(e) => _handleFilterChange(filter.key, e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">{filter.placeholder || 'Alle'}</option>
|
<option value="">{filter.placeholder || t('Alle')}</option>
|
||||||
{filter.options?.map(opt => (
|
{filter.options?.map(opt => (
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export const UserSection: React.FC = () => {
|
||||||
onClick={handleBilling}
|
onClick={handleBilling}
|
||||||
>
|
>
|
||||||
<span className={styles.menuIcon}>💰</span>
|
<span className={styles.menuIcon}>💰</span>
|
||||||
Guthaben
|
{t('Guthaben')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -104,7 +104,7 @@ export const UserSection: React.FC = () => {
|
||||||
onClick={handleSettings}
|
onClick={handleSettings}
|
||||||
>
|
>
|
||||||
<span className={styles.menuIcon}>⚙️</span>
|
<span className={styles.menuIcon}>⚙️</span>
|
||||||
Einstellungen
|
{t('Einstellungen')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{onboardingHidden && (
|
{onboardingHidden && (
|
||||||
|
|
@ -113,7 +113,7 @@ export const UserSection: React.FC = () => {
|
||||||
onClick={handleOnboarding}
|
onClick={handleOnboarding}
|
||||||
>
|
>
|
||||||
<span className={styles.menuIcon}>{'\uD83E\uDDED'}</span>
|
<span className={styles.menuIcon}>{'\uD83E\uDDED'}</span>
|
||||||
Onboarding-Assistent
|
{t('Onboarding-Assistent')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -122,7 +122,7 @@ export const UserSection: React.FC = () => {
|
||||||
onClick={handleLegal}
|
onClick={handleLegal}
|
||||||
>
|
>
|
||||||
<span className={styles.menuIcon}>📜</span>
|
<span className={styles.menuIcon}>📜</span>
|
||||||
Rechtliche Hinweise
|
{t('Rechtliche Hinweise')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={styles.menuDivider} />
|
<div className={styles.menuDivider} />
|
||||||
|
|
@ -133,7 +133,7 @@ export const UserSection: React.FC = () => {
|
||||||
disabled={isLoggingOut}
|
disabled={isLoggingOut}
|
||||||
>
|
>
|
||||||
<span className={styles.menuIcon}>🚪</span>
|
<span className={styles.menuIcon}>🚪</span>
|
||||||
{isLoggingOut ? 'Abmelden...' : 'Abmelden'}
|
{isLoggingOut ? t('Abmelden...') : t('Abmelden')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -176,13 +176,13 @@ export const UserSection: React.FC = () => {
|
||||||
|
|
||||||
<div className={styles.legalLinks}>
|
<div className={styles.legalLinks}>
|
||||||
<a href="/poweron-privacy.html" target="_blank" rel="noopener noreferrer">
|
<a href="/poweron-privacy.html" target="_blank" rel="noopener noreferrer">
|
||||||
Datenschutzrichtlinie
|
{t('Datenschutzrichtlinie')}
|
||||||
</a>
|
</a>
|
||||||
<a href="/poweron-terms.html" target="_blank" rel="noopener noreferrer">
|
<a href="/poweron-terms.html" target="_blank" rel="noopener noreferrer">
|
||||||
Nutzungsbedingungen
|
{t('Nutzungsbedingungen')}
|
||||||
</a>
|
</a>
|
||||||
<a href="/poweron-home.html" target="_blank" rel="noopener noreferrer">
|
<a href="/poweron-home.html" target="_blank" rel="noopener noreferrer">
|
||||||
Über PowerOn
|
{t('Über PowerOn')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,32 +20,32 @@ const typeIcons: Record<string, React.ReactNode> = {
|
||||||
mention: <FaExclamationTriangle />
|
mention: <FaExclamationTriangle />
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format timestamp to relative time (Unix seconds)
|
|
||||||
function formatRelativeTime(timestamp: number): string {
|
|
||||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const now = Date.now() / 1000;
|
|
||||||
const diff = now - timestamp;
|
|
||||||
|
|
||||||
if (diff < 60) return 'Gerade eben';
|
|
||||||
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`;
|
|
||||||
if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`;
|
|
||||||
if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`;
|
|
||||||
|
|
||||||
const date = new Date(timestamp * 1000);
|
|
||||||
if (Number.isNaN(date.getTime())) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return date.toLocaleDateString('de-DE');
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotificationBellProps {
|
interface NotificationBellProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationBell: React.FC<NotificationBellProps> = ({ className }) => {
|
export const NotificationBell: React.FC<NotificationBellProps> = ({ className }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
|
const formatRelativeTime = useCallback((timestamp: number): string => {
|
||||||
|
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
const diff = now - timestamp;
|
||||||
|
|
||||||
|
if (diff < 60) return t('Gerade eben');
|
||||||
|
if (diff < 3600) return t('vor {minutes} Min.', { minutes: String(Math.floor(diff / 60)) });
|
||||||
|
if (diff < 86400) return t('vor {hours} Std.', { hours: String(Math.floor(diff / 3600)) });
|
||||||
|
if (diff < 604800) return t('vor {days} Tagen', { days: String(Math.floor(diff / 86400)) });
|
||||||
|
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return date.toLocaleDateString('de-DE');
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
notifications,
|
notifications,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
|
|
@ -144,7 +144,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
||||||
<button
|
<button
|
||||||
className={styles.bellButton}
|
className={styles.bellButton}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
aria-label={`Benachrichtigungen ${unreadCount > 0 ? `(${unreadCount} ungelesen)` : ''}`}
|
aria-label={unreadCount > 0 ? t('Benachrichtigungen ({count} ungelesen)', { count: String(unreadCount) }) : t('Benachrichtigungen')}
|
||||||
>
|
>
|
||||||
<FaBell className={styles.bellIcon} />
|
<FaBell className={styles.bellIcon} />
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
|
|
@ -165,7 +165,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
||||||
className={styles.markAllRead}
|
className={styles.markAllRead}
|
||||||
onClick={() => markAllAsRead()}
|
onClick={() => markAllAsRead()}
|
||||||
>
|
>
|
||||||
Alle als gelesen markieren
|
{t('Alle als gelesen markieren')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -201,7 +201,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
||||||
{actionSuccess === notification.id && (
|
{actionSuccess === notification.id && (
|
||||||
<div className={styles.successOverlay}>
|
<div className={styles.successOverlay}>
|
||||||
<FaCheckCircle />
|
<FaCheckCircle />
|
||||||
<span>{notification.actionResult || 'Erfolgreich'}</span>
|
<span>{notification.actionResult || t('Erfolgreich')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import OnboardingWizard from './OnboardingWizard';
|
import OnboardingWizard from './OnboardingWizard';
|
||||||
|
|
@ -19,13 +19,6 @@ interface OnboardingAssistantProps {
|
||||||
|
|
||||||
const _STORAGE_KEY = 'onboarding_hidden';
|
const _STORAGE_KEY = 'onboarding_hidden';
|
||||||
|
|
||||||
const _CALLOUTS: Record<string, string> = {
|
|
||||||
mandate: 'Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.',
|
|
||||||
feature: 'Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.',
|
|
||||||
connection: 'Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.',
|
|
||||||
chat: 'Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function _isOnboardingHidden(): boolean {
|
export function _isOnboardingHidden(): boolean {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(_STORAGE_KEY) === 'true';
|
return localStorage.getItem(_STORAGE_KEY) === 'true';
|
||||||
|
|
@ -48,6 +41,12 @@ function _hideOnboarding(): void {
|
||||||
|
|
||||||
const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => {
|
const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const callouts = useMemo(() => ({
|
||||||
|
mandate: t('Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.'),
|
||||||
|
feature: t('Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.'),
|
||||||
|
connection: t('Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.'),
|
||||||
|
chat: t('Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.'),
|
||||||
|
}), [t]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [hidden, setHidden] = useState(() => _isOnboardingHidden());
|
const [hidden, setHidden] = useState(() => _isOnboardingHidden());
|
||||||
|
|
@ -99,7 +98,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
id: 'mandate',
|
id: 'mandate',
|
||||||
label: t('Mandat einrichten'),
|
label: t('Mandat einrichten'),
|
||||||
description: hasAdminMandate
|
description: hasAdminMandate
|
||||||
? 'Dein Mandant ist eingerichtet.'
|
? t('Dein Mandant ist eingerichtet.')
|
||||||
: hasFeature
|
: hasFeature
|
||||||
? t('Du bist Mitglied eines Mandanten')
|
? t('Du bist Mitglied eines Mandanten')
|
||||||
: t('Erstelle deinen Arbeitsbereich'),
|
: t('Erstelle deinen Arbeitsbereich'),
|
||||||
|
|
@ -166,7 +165,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = location.state as { showOnboarding?: number } | null;
|
const state = location.state as { showOnboarding?: number } | null;
|
||||||
|
|
@ -215,7 +214,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
<div>
|
<div>
|
||||||
<h3 style={{ margin: 0, fontSize: '1rem' }}>{t('Willkommen bei Poweron')}</h3>
|
<h3 style={{ margin: 0, fontSize: '1rem' }}>{t('Willkommen bei Poweron')}</h3>
|
||||||
<p style={{ margin: '4px 0 0', fontSize: '0.85rem', color: 'var(--text-secondary, #6b7280)' }}>
|
<p style={{ margin: '4px 0 0', fontSize: '0.85rem', color: 'var(--text-secondary, #6b7280)' }}>
|
||||||
{completedCount} von {steps.length} Schritten abgeschlossen
|
{t('{completed} von {total} Schritten abgeschlossen', { completed: String(completedCount), total: String(steps.length) })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -263,14 +262,14 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
<span style={{ fontSize: '0.8rem', color: 'var(--accent, #4f46e5)', fontWeight: 500 }}>{'\u2192'}</span>
|
<span style={{ fontSize: '0.8rem', color: 'var(--accent, #4f46e5)', fontWeight: 500 }}>{'\u2192'}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isNextStep && _CALLOUTS[step.id] && (
|
{isNextStep && callouts[step.id as keyof typeof callouts] && (
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 4, marginLeft: 34, padding: '6px 10px',
|
marginTop: 4, marginLeft: 34, padding: '6px 10px',
|
||||||
fontSize: '0.78rem', color: 'var(--accent, #4f46e5)',
|
fontSize: '0.78rem', color: 'var(--accent, #4f46e5)',
|
||||||
background: 'rgba(79, 70, 229, 0.06)', borderRadius: 6,
|
background: 'rgba(79, 70, 229, 0.06)', borderRadius: 6,
|
||||||
borderLeft: '3px solid var(--accent, #4f46e5)',
|
borderLeft: '3px solid var(--accent, #4f46e5)',
|
||||||
}}>
|
}}>
|
||||||
{_CALLOUTS[step.id]}
|
{callouts[step.id as keyof typeof callouts]}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -290,7 +289,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
onChange={(e) => setDontShowAgain(e.target.checked)}
|
onChange={(e) => setDontShowAgain(e.target.checked)}
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
/>
|
/>
|
||||||
Nicht wieder anzeigen
|
{t('Nicht wieder anzeigen')}
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={_handleDismiss}
|
onClick={_handleDismiss}
|
||||||
|
|
@ -304,7 +303,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Schliessen
|
{t('Schliessen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
window.dispatchEvent(new CustomEvent('features-changed'));
|
window.dispatchEvent(new CustomEvent('features-changed'));
|
||||||
onComplete();
|
onComplete();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.response?.data?.detail || 'Fehler bei der Einrichtung');
|
setError(err?.response?.data?.detail || t('Fehler bei der Einrichtung'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>{t('Mandant erstellen')}</h2>
|
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>{t('Mandant erstellen')}</h2>
|
||||||
<p style={{ color: 'var(--text-secondary, #666)', margin: '0 0 24px' }}>
|
<p style={{ color: 'var(--text-secondary, #666)', margin: '0 0 24px' }}>
|
||||||
Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.
|
{t('Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
|
||||||
|
|
@ -62,7 +62,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
<div>
|
<div>
|
||||||
<strong>{t('Kostenlos testen')}</strong>
|
<strong>{t('Kostenlos testen')}</strong>
|
||||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
||||||
7 Tage gratis, danach flexibel upgraden
|
{t('7 Tage gratis, danach flexibel upgraden')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -77,7 +77,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
<div>
|
<div>
|
||||||
<strong>{t('Standard monatlich')}</strong>
|
<strong>{t('Standard monatlich')}</strong>
|
||||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
||||||
Team-Workspace mit vollem Funktionsumfang
|
{t('Team-Workspace mit vollem Funktionsumfang')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -85,7 +85,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<div style={{ marginBottom: '20px' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>
|
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>
|
||||||
Name des Mandanten <span style={{ fontWeight: 400, color: 'var(--text-secondary, #666)' }}>(optional)</span>
|
{t('Name des Mandanten')} <span style={{ fontWeight: 400, color: 'var(--text-secondary, #666)' }}>({t('optional')})</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text" value={mandateName}
|
type="text" value={mandateName}
|
||||||
|
|
@ -106,7 +106,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
padding: '10px 20px', borderRadius: '6px', border: '1px solid var(--border, #d1d5db)',
|
padding: '10px 20px', borderRadius: '6px', border: '1px solid var(--border, #d1d5db)',
|
||||||
background: 'transparent', cursor: 'pointer',
|
background: 'transparent', cursor: 'pointer',
|
||||||
}}>
|
}}>
|
||||||
Abbrechen
|
{t('Abbrechen')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={_handleSubmit} disabled={loading}
|
<button onClick={_handleSubmit} disabled={loading}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,7 @@ export function _toBackendProviders(
|
||||||
return _resolveProviders(selection, allowedProviders);
|
return _resolveProviders(selection, allowedProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider display names
|
const _PROVIDER_LABEL_KEYS: Record<string, string> = {
|
||||||
const PROVIDER_LABELS: Record<string, string> = {
|
|
||||||
anthropic: 'Anthropic (Claude)',
|
anthropic: 'Anthropic (Claude)',
|
||||||
openai: 'OpenAI (GPT)',
|
openai: 'OpenAI (GPT)',
|
||||||
mistral: 'Mistral (Le Chat)',
|
mistral: 'Mistral (Le Chat)',
|
||||||
|
|
@ -88,6 +87,11 @@ const PROVIDER_LABELS: Record<string, string> = {
|
||||||
internal: 'Internal',
|
internal: 'Internal',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function _providerLabel(provider: string, t: (key: string) => string): string {
|
||||||
|
const key = _PROVIDER_LABEL_KEYS[provider];
|
||||||
|
return key ? t(key) : provider;
|
||||||
|
}
|
||||||
|
|
||||||
const PROVIDER_ICONS: Record<string, string> = {
|
const PROVIDER_ICONS: Record<string, string> = {
|
||||||
anthropic: '🤖',
|
anthropic: '🤖',
|
||||||
openai: '💬',
|
openai: '💬',
|
||||||
|
|
@ -115,10 +119,11 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
label = 'AI-Provider',
|
label,
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const resolvedLabel = label ?? t('AI-Provider');
|
||||||
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -130,13 +135,13 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
|
||||||
const providerOptions = useMemo(() => {
|
const providerOptions = useMemo(() => {
|
||||||
return allowedProviders.map((provider) => ({
|
return allowedProviders.map((provider) => ({
|
||||||
value: provider,
|
value: provider,
|
||||||
label: `${PROVIDER_ICONS[provider] || '🔌'} ${PROVIDER_LABELS[provider] || provider}`,
|
label: `${PROVIDER_ICONS[provider] || '🔌'} ${_providerLabel(provider, t)}`,
|
||||||
}));
|
}));
|
||||||
}, [allowedProviders]);
|
}, [allowedProviders, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.providerSelect} ${className || ''}`}>
|
<div className={`${styles.providerSelect} ${className || ''}`}>
|
||||||
{showLabel && <label className={styles.label}>{label}</label>}
|
{showLabel && <label className={styles.label}>{resolvedLabel}</label>}
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
|
@ -174,12 +179,13 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
label = 'AI-Provider',
|
label,
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
defaultExpanded = false,
|
defaultExpanded = false,
|
||||||
excludeByDefault = [],
|
excludeByDefault = [],
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const resolvedLabel = label ?? t('AI-Provider');
|
||||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||||
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
|
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -264,13 +270,13 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
}, [effectiveSelection, noneSelected]);
|
}, [effectiveSelection, noneSelected]);
|
||||||
|
|
||||||
const summaryHint = useMemo(() => {
|
const summaryHint = useMemo(() => {
|
||||||
if (noneSelected) return 'Kein Provider ausgewählt';
|
if (noneSelected) return t('Kein Provider ausgewählt');
|
||||||
if (allSelected) return 'Alle Provider aktiv (dynamisch)';
|
if (allSelected) return t('Alle Provider aktiv (dynamisch)');
|
||||||
if (selection.include.includes(PROVIDER_ALL)) {
|
if (selection.include.includes(PROVIDER_ALL)) {
|
||||||
return `Alle ausser ${selection.exclude.length} Provider`;
|
return t('Alle ausser {count} Provider', { count: String(selection.exclude.length) });
|
||||||
}
|
}
|
||||||
return `${effectiveSelection.length} von ${allowedProviders.length} Provider`;
|
return t('{n} von {total} Provider', { n: String(effectiveSelection.length), total: String(allowedProviders.length) });
|
||||||
}, [noneSelected, allSelected, selection, effectiveSelection, allowedProviders]);
|
}, [noneSelected, allSelected, selection, effectiveSelection, allowedProviders, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -289,7 +295,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className={styles.dropdownContent}>
|
<div className={styles.dropdownContent}>
|
||||||
{showLabel && <div className={styles.dropdownHeader}>{label}</div>}
|
{showLabel && <div className={styles.dropdownHeader}>{resolvedLabel}</div>}
|
||||||
|
|
||||||
<div className={styles.selectActions}>
|
<div className={styles.selectActions}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -298,7 +304,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`${styles.actionButton} ${allSelected ? styles.active : ''}`}
|
className={`${styles.actionButton} ${allSelected ? styles.active : ''}`}
|
||||||
>
|
>
|
||||||
Alle
|
{t('Alle')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -319,7 +325,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
/>
|
/>
|
||||||
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
||||||
<span className={styles.providerName}>
|
<span className={styles.providerName}>
|
||||||
{PROVIDER_LABELS[provider] || provider}
|
{_providerLabel(provider, t)}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
|
@ -355,7 +361,7 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
|
||||||
<div className={`${styles.providerBadges} ${className || ''}`}>
|
<div className={`${styles.providerBadges} ${className || ''}`}>
|
||||||
{providers.map((provider) => (
|
{providers.map((provider) => (
|
||||||
<span key={provider} className={styles.badge}>
|
<span key={provider} className={styles.badge}>
|
||||||
{PROVIDER_ICONS[provider] || '🔌'} {PROVIDER_LABELS[provider] || provider}
|
{PROVIDER_ICONS[provider] || '🔌'} {_providerLabel(provider, t)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -80,20 +80,20 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.preview}>
|
<div className={styles.preview}>
|
||||||
<div className={styles.previewHeader}>
|
<div className={styles.previewHeader}>
|
||||||
<h4 className={styles.previewTitle}>Export-Vorschau</h4>
|
<h4 className={styles.previewTitle}>{t('Export-Vorschau')}</h4>
|
||||||
<button className={styles.closeButton} onClick={onClose}>✕</button>
|
<button className={styles.closeButton} onClick={onClose}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.previewContent}>
|
<div className={styles.previewContent}>
|
||||||
<div className={styles.previewSection}>
|
<div className={styles.previewSection}>
|
||||||
<h5>Scope</h5>
|
<h5>{t('Scope')}</h5>
|
||||||
<ul className={styles.previewList}>
|
<ul className={styles.previewList}>
|
||||||
<li><strong>Typ:</strong> {data.scope.type}</li>
|
<li><strong>{t('Typ:')}</strong> {data.scope.type}</li>
|
||||||
{data.scope.mandateName && <li><strong>{t('Mandant')}</strong> {data.scope.mandateName}</li>}
|
{data.scope.mandateName && <li><strong>{t('Mandant')}</strong> {data.scope.mandateName}</li>}
|
||||||
{data.scope.featureCode && <li><strong>Feature:</strong> {data.scope.featureCode}</li>}
|
{data.scope.featureCode && <li><strong>{t('Feature:')}</strong> {data.scope.featureCode}</li>}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.previewSection}>
|
<div className={styles.previewSection}>
|
||||||
<h5>Rollen ({data.roles.length})</h5>
|
<h5>{t('Rollen ({count})', { count: String(data.roles.length) })}</h5>
|
||||||
<ul className={styles.previewList}>
|
<ul className={styles.previewList}>
|
||||||
{data.roles.slice(0, 5).map((role, i) => (
|
{data.roles.slice(0, 5).map((role, i) => (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
|
|
@ -102,21 +102,21 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{data.roles.length > 5 && (
|
{data.roles.length > 5 && (
|
||||||
<li className={styles.moreItems}>... und {data.roles.length - 5} weitere</li>
|
<li className={styles.moreItems}>{t('... und {count} weitere', { count: String(data.roles.length - 5) })}</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.previewSection}>
|
<div className={styles.previewSection}>
|
||||||
<h5>Regeln ({data.accessRules.length})</h5>
|
<h5>{t('Regeln ({count})', { count: String(data.accessRules.length) })}</h5>
|
||||||
<ul className={styles.previewList}>
|
<ul className={styles.previewList}>
|
||||||
{data.accessRules.slice(0, 5).map((rule, i) => (
|
{data.accessRules.slice(0, 5).map((rule, i) => (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<span className={styles.contextBadge}>{rule.context}</span>
|
<span className={styles.contextBadge}>{rule.context}</span>
|
||||||
<code>{rule.item || '(global)'}</code>
|
<code>{rule.item || t('(global)')}</code>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{data.accessRules.length > 5 && (
|
{data.accessRules.length > 5 && (
|
||||||
<li className={styles.moreItems}>... und {data.accessRules.length - 5} weitere</li>
|
<li className={styles.moreItems}>{t('... und {count} weitere', { count: String(data.accessRules.length - 5) })}</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -154,7 +154,7 @@ const ImportResult: React.FC<ImportResultProps> = ({ result, onClose }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.resultContent}>
|
<div className={styles.resultContent}>
|
||||||
<ul className={styles.resultStats}>
|
<ul className={styles.resultStats}>
|
||||||
<li><strong>Modus:</strong> {importModes.find(m => m.value === result.mode)?.label}</li>
|
<li><strong>{t('Modus:')}</strong> {importModes.find(m => m.value === result.mode)?.label}</li>
|
||||||
<li><strong>{t('Rollen erstellt')}</strong> {result.rolesCreated}</li>
|
<li><strong>{t('Rollen erstellt')}</strong> {result.rolesCreated}</li>
|
||||||
<li><strong>{t('Rollen aktualisiert')}</strong> {result.rolesUpdated}</li>
|
<li><strong>{t('Rollen aktualisiert')}</strong> {result.rolesUpdated}</li>
|
||||||
<li><strong>{t('Regeln erstellt')}</strong> {result.rulesCreated}</li>
|
<li><strong>{t('Regeln erstellt')}</strong> {result.rulesCreated}</li>
|
||||||
|
|
@ -238,7 +238,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setImportData(result.data);
|
setImportData(result.data);
|
||||||
} else {
|
} else {
|
||||||
setParseError(result.error || 'Fehler beim Parsen');
|
setParseError(result.error || t('Fehler beim Parsen'));
|
||||||
setImportData(null);
|
setImportData(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -289,13 +289,14 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<FaFileExport className={styles.sectionIcon} />
|
<FaFileExport className={styles.sectionIcon} />
|
||||||
<h3 className={styles.sectionTitle}>Export</h3>
|
<h3 className={styles.sectionTitle}>{t('Export')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.sectionContent}>
|
<div className={styles.sectionContent}>
|
||||||
<p className={styles.sectionDescription}>
|
<p className={styles.sectionDescription}>
|
||||||
Exportiert alle Rollen und Berechtigungen
|
{t('Exportiert alle Rollen und Berechtigungen')}{' '}
|
||||||
{isGlobal ? ' der globalen Templates' : ` des Mandanten "${mandateName || mandateId}"`}
|
{isGlobal ? t('der globalen Templates') : t('des Mandanten "{name}"', { name: String(mandateName || mandateId || '') })}
|
||||||
{featureCode ? ` für Feature "${featureCode}"` : ''} als JSON-Datei.
|
{featureCode ? <> {t('für Feature "{code}"', { code: featureCode })}</> : null}{' '}
|
||||||
|
{t('als JSON-Datei.')}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
className={styles.primaryButton}
|
className={styles.primaryButton}
|
||||||
|
|
@ -304,11 +305,11 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
>
|
>
|
||||||
{exporting ? (
|
{exporting ? (
|
||||||
<>
|
<>
|
||||||
<FaSpinner className="spinning" /> Exportieren...
|
<FaSpinner className="spinning" /> {t('Exportieren...')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaDownload /> RBAC exportieren
|
<FaDownload /> {t('RBAC exportieren')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -319,7 +320,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<FaFileImport className={styles.sectionIcon} />
|
<FaFileImport className={styles.sectionIcon} />
|
||||||
<h3 className={styles.sectionTitle}>Import</h3>
|
<h3 className={styles.sectionTitle}>{t('Import')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.sectionContent}>
|
<div className={styles.sectionContent}>
|
||||||
{/* File Upload */}
|
{/* File Upload */}
|
||||||
|
|
@ -368,14 +369,14 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
<div className={styles.importInfo}>
|
<div className={styles.importInfo}>
|
||||||
<div className={styles.importStats}>
|
<div className={styles.importStats}>
|
||||||
<span><strong>{t('Rollen')}</strong> {importData.roles.length}</span>
|
<span><strong>{t('Rollen')}</strong> {importData.roles.length}</span>
|
||||||
<span><strong>Regeln:</strong> {importData.accessRules.length}</span>
|
<span><strong>{t('Regeln:')}</strong> {importData.accessRules.length}</span>
|
||||||
<span><strong>Quelle:</strong> {importData.scope.type}</span>
|
<span><strong>{t('Quelle:')}</strong> {importData.scope.type}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={styles.previewButton}
|
className={styles.previewButton}
|
||||||
onClick={() => setShowPreview(true)}
|
onClick={() => setShowPreview(true)}
|
||||||
>
|
>
|
||||||
<FaEye /> Vorschau
|
<FaEye /> {t('Vorschau')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -383,7 +384,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
{/* Import Mode Selection */}
|
{/* Import Mode Selection */}
|
||||||
{importData && (
|
{importData && (
|
||||||
<div className={styles.importModeSection}>
|
<div className={styles.importModeSection}>
|
||||||
<h4 className={styles.importModeTitle}>Import-Modus</h4>
|
<h4 className={styles.importModeTitle}>{t('Import-Modus')}</h4>
|
||||||
<div className={styles.importModes}>
|
<div className={styles.importModes}>
|
||||||
{importModes.map(mode => (
|
{importModes.map(mode => (
|
||||||
<label
|
<label
|
||||||
|
|
@ -416,11 +417,11 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
>
|
>
|
||||||
{importing ? (
|
{importing ? (
|
||||||
<>
|
<>
|
||||||
<FaSpinner className="spinning" /> Importieren...
|
<FaSpinner className="spinning" /> {t('Importieren...')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaUpload /> RBAC importieren
|
<FaUpload /> {t('RBAC importieren')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -430,7 +431,8 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
||||||
{importMode === 'replace' && importData && (
|
{importMode === 'replace' && importData && (
|
||||||
<div className={styles.warningMessage}>
|
<div className={styles.warningMessage}>
|
||||||
<FaExclamationTriangle />
|
<FaExclamationTriangle />
|
||||||
<strong>Achtung:</strong> Im Modus "Ersetzen" werden alle bestehenden Rollen und Regeln gelöscht!
|
<strong>{t('Achtung:')}</strong>{' '}
|
||||||
|
{t('Im Modus Ersetzen werden alle bestehenden Rollen und Regeln gelöscht!')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
|
||||||
setAutocompleteError(null); // Clear any previous errors on success
|
setAutocompleteError(null); // Clear any previous errors on success
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('❌ [AddressAutocomplete] Error in performSearch:', err);
|
console.error('❌ [AddressAutocomplete] Error in performSearch:', err);
|
||||||
const errorMessage = err?.response?.data?.detail || err?.message || 'Fehler beim Laden der Adressvorschläge';
|
const errorMessage = err?.response?.data?.detail || err?.message || t('Fehler beim Laden der Adressvorschläge');
|
||||||
setAutocompleteError(errorMessage);
|
setAutocompleteError(errorMessage);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setShowSuggestions(true); // Show dropdown to display error
|
setShowSuggestions(true); // Show dropdown to display error
|
||||||
|
|
@ -103,7 +103,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [minQueryLength, maxSuggestions]);
|
}, [minQueryLength, maxSuggestions, t]);
|
||||||
|
|
||||||
// Handle input change with debouncing
|
// Handle input change with debouncing
|
||||||
const handleInputChange = useCallback((newValue: string) => {
|
const handleInputChange = useCallback((newValue: string) => {
|
||||||
|
|
@ -286,7 +286,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
|
||||||
)}
|
)}
|
||||||
{!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && (
|
{!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && (
|
||||||
<li className={styles.suggestionItem}>
|
<li className={styles.suggestionItem}>
|
||||||
<span className={styles.noResultsText}>{t('no addresses found')}</span>
|
<span className={styles.noResultsText}>{t('Keine Adressen gefunden')}</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{!isLoading && suggestions.map((suggestion, index) => (
|
{!isLoading && suggestions.map((suggestion, index) => (
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
||||||
<div className={styles.bauvorschriftenHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
<div className={styles.bauvorschriftenHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
||||||
<h4 className={styles.subSectionTitle}>
|
<h4 className={styles.subSectionTitle}>
|
||||||
<FaRuler style={{ marginRight: '8px', display: 'inline' }} />
|
<FaRuler style={{ marginRight: '8px', display: 'inline' }} />
|
||||||
Bauvorschriften - {bauvorschriften.zonenbezeichnung}
|
{t('Bauvorschriften')} – {bauvorschriften.zonenbezeichnung}
|
||||||
</h4>
|
</h4>
|
||||||
<button className={styles.expandButton}>
|
<button className={styles.expandButton}>
|
||||||
{isExpanded ? <FaChevronUp /> : <FaChevronDown />}
|
{isExpanded ? <FaChevronUp /> : <FaChevronDown />}
|
||||||
|
|
@ -48,7 +48,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
||||||
)}
|
)}
|
||||||
{bauvorschriften.vollgeschosse !== undefined && bauvorschriften.vollgeschosse !== null && (
|
{bauvorschriften.vollgeschosse !== undefined && bauvorschriften.vollgeschosse !== null && (
|
||||||
<div className={styles.bauvorschriftItem}>
|
<div className={styles.bauvorschriftItem}>
|
||||||
<span className={styles.label}>Vollgeschosse:</span>
|
<span className={styles.label}>{t('Vollgeschosse')}</span>
|
||||||
<span className={styles.value}>{bauvorschriften.vollgeschosse}</span>
|
<span className={styles.value}>{bauvorschriften.vollgeschosse}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -60,7 +60,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
||||||
)}
|
)}
|
||||||
{bauvorschriften.grenzabstand !== undefined && bauvorschriften.grenzabstand !== null && (
|
{bauvorschriften.grenzabstand !== undefined && bauvorschriften.grenzabstand !== null && (
|
||||||
<div className={styles.bauvorschriftItem}>
|
<div className={styles.bauvorschriftItem}>
|
||||||
<span className={styles.label}>Grenzabstand:</span>
|
<span className={styles.label}>{t('Grenzabstand')}</span>
|
||||||
<span className={styles.value}>{bauvorschriften.grenzabstand} m</span>
|
<span className={styles.value}>{bauvorschriften.grenzabstand} m</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -93,7 +93,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
||||||
className={styles.sourceLinkButton}
|
className={styles.sourceLinkButton}
|
||||||
>
|
>
|
||||||
<FaFilePdf style={{ marginRight: '8px' }} />
|
<FaFilePdf style={{ marginRight: '8px' }} />
|
||||||
Nutzungsplan öffnen
|
{t('Nutzungsplan öffnen')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -101,7 +101,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
||||||
{bauvorschriften.extraktionsDatum && (
|
{bauvorschriften.extraktionsDatum && (
|
||||||
<div className={styles.bauvorschriftenFooter}>
|
<div className={styles.bauvorschriftenFooter}>
|
||||||
<span className={styles.lastUpdated}>
|
<span className={styles.lastUpdated}>
|
||||||
Extrahiert: {new Date(bauvorschriften.extraktionsDatum).toLocaleString('de-CH')}
|
{t('Extrahiert')}: {new Date(bauvorschriften.extraktionsDatum).toLocaleString('de-CH')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
const CreateButton: React.FC<CreateButtonProps> = ({
|
const CreateButton: React.FC<CreateButtonProps> = ({
|
||||||
onCreate,
|
onCreate,
|
||||||
fields,
|
fields,
|
||||||
popupTitle = 'Create New Item',
|
popupTitle = 'Neues Element erstellen',
|
||||||
popupSize = 'medium',
|
popupSize = 'medium',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|
@ -131,7 +131,7 @@ const CreateButton: React.FC<CreateButtonProps> = ({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Creation failed:', error);
|
console.error('Creation failed:', error);
|
||||||
if (onError) {
|
if (onError) {
|
||||||
onError(error.message || 'Creation failed');
|
onError(error.message || t('Erstellung fehlgeschlagen'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { UploadButtonProps } from '../ButtonTypes';
|
import { UploadButtonProps } from '../ButtonTypes';
|
||||||
import Button from '../Button';
|
import Button from '../Button';
|
||||||
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
const UploadButton: React.FC<UploadButtonProps> = ({
|
const UploadButton: React.FC<UploadButtonProps> = ({
|
||||||
onUpload,
|
onUpload,
|
||||||
|
|
@ -71,7 +72,7 @@ const UploadButton: React.FC<UploadButtonProps> = ({
|
||||||
{isUploading && (
|
{isUploading && (
|
||||||
<div className="spinnerIcon" style={{ marginRight: '8px' }} />
|
<div className="spinnerIcon" style={{ marginRight: '8px' }} />
|
||||||
)}
|
)}
|
||||||
{children || (isUploading ? 'Uploading...' : 'Upload File')}
|
{children || (isUploading ? t('Wird hochgeladen…') : t('Datei hochladen'))}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export function ConnectedFilesList({
|
||||||
previewingFiles = new Set(),
|
previewingFiles = new Set(),
|
||||||
removingFiles = new Set(),
|
removingFiles = new Set(),
|
||||||
workflowId: _workflowId,
|
workflowId: _workflowId,
|
||||||
emptyMessage = 'No files connected to this workflow'
|
emptyMessage = 'Keine Dateien mit diesem Workflow verbunden'
|
||||||
}: ConnectedFilesListProps) {
|
}: ConnectedFilesListProps) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
// Combine workflow files and pending files, deduplicating by fileId
|
// Combine workflow files and pending files, deduplicating by fileId
|
||||||
|
|
@ -253,7 +253,7 @@ export function ConnectedFilesList({
|
||||||
)}
|
)}
|
||||||
{isPendingFile && (
|
{isPendingFile && (
|
||||||
<span style={{ fontSize: '0.75rem', color: '#4CAF50', fontWeight: 500 }}>
|
<span style={{ fontSize: '0.75rem', color: '#4CAF50', fontWeight: 500 }}>
|
||||||
• Attached
|
• {t('Angehängt')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { IconType } from 'react-icons';
|
||||||
import { IoChevronDown, IoClose } from 'react-icons/io5';
|
import { IoChevronDown, IoClose } from 'react-icons/io5';
|
||||||
import styles from './DropdownSelect.module.css';
|
import styles from './DropdownSelect.module.css';
|
||||||
import { ButtonVariant, ButtonSize } from '../Button/ButtonTypes';
|
import { ButtonVariant, ButtonSize } from '../Button/ButtonTypes';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export interface DropdownSelectItem<T = any> {
|
export interface DropdownSelectItem<T = any> {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
|
|
@ -37,8 +38,8 @@ function DropdownSelect<T = any>({
|
||||||
items = [],
|
items = [],
|
||||||
selectedItemId,
|
selectedItemId,
|
||||||
onSelect,
|
onSelect,
|
||||||
placeholder = 'Select an item',
|
placeholder,
|
||||||
emptyMessage = 'No items available',
|
emptyMessage,
|
||||||
headerText,
|
headerText,
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
|
|
@ -54,6 +55,9 @@ function DropdownSelect<T = any>({
|
||||||
showClearButton = true,
|
showClearButton = true,
|
||||||
clearButtonLabel
|
clearButtonLabel
|
||||||
}: DropdownSelectProps<T>) {
|
}: DropdownSelectProps<T>) {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const resolvedPlaceholder = placeholder ?? t('Element auswählen');
|
||||||
|
const resolvedEmptyMessage = emptyMessage ?? t('Keine Einträge verfügbar');
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
@ -116,7 +120,7 @@ function DropdownSelect<T = any>({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.buttonSpinner} />
|
<div className={styles.buttonSpinner} />
|
||||||
<span>{placeholder}</span>
|
<span>{resolvedPlaceholder}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +138,7 @@ function DropdownSelect<T = any>({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Icon && <Icon className={styles.buttonIcon} />}
|
{Icon && <Icon className={styles.buttonIcon} />}
|
||||||
<span className={styles.buttonText}>{placeholder}</span>
|
<span className={styles.buttonText}>{resolvedPlaceholder}</span>
|
||||||
<IoChevronDown className={`${styles.chevronIcon} ${isOpen ? styles.chevronOpen : ''}`} />
|
<IoChevronDown className={`${styles.chevronIcon} ${isOpen ? styles.chevronOpen : ''}`} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -153,7 +157,7 @@ function DropdownSelect<T = any>({
|
||||||
className={buttonClasses}
|
className={buttonClasses}
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
title={clearButtonLabel || `Clear selection: ${selectedItem.label}`}
|
title={clearButtonLabel || t('Auswahl aufheben: {label}', { label: selectedItem.label })}
|
||||||
>
|
>
|
||||||
{Icon && <Icon className={styles.buttonIcon} />}
|
{Icon && <Icon className={styles.buttonIcon} />}
|
||||||
<span className={styles.buttonText}>
|
<span className={styles.buttonText}>
|
||||||
|
|
@ -197,7 +201,7 @@ function DropdownSelect<T = any>({
|
||||||
|
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<div className={styles.dropdownEmpty}>
|
<div className={styles.dropdownEmpty}>
|
||||||
{emptyMessage}
|
{resolvedEmptyMessage}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.dropdownItems}>
|
<div className={styles.dropdownItems}>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||||
import { Button, TextField } from '../index';
|
import { Button, TextField } from '../index';
|
||||||
import { FaLocationArrow } from 'react-icons/fa';
|
import { FaLocationArrow } from 'react-icons/fa';
|
||||||
import styles from './LocationInput.module.css';
|
import styles from './LocationInput.module.css';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export interface LocationInputProps {
|
export interface LocationInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -20,12 +21,15 @@ const LocationInput: React.FC<LocationInputProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
onUseCurrentLocation,
|
onUseCurrentLocation,
|
||||||
isGettingLocation = false,
|
isGettingLocation = false,
|
||||||
placeholder = 'Kanton, Gemeinde, Adresse oder Parzelle',
|
placeholder,
|
||||||
label = 'Standort',
|
label,
|
||||||
error,
|
error,
|
||||||
helperText,
|
helperText,
|
||||||
disabled = false
|
disabled = false
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const resolvedPlaceholder = placeholder ?? t('Kanton, Gemeinde, Adresse oder Parzelle');
|
||||||
|
const resolvedLabel = label ?? t('Standort');
|
||||||
const [isRequestingLocation, setIsRequestingLocation] = useState(false);
|
const [isRequestingLocation, setIsRequestingLocation] = useState(false);
|
||||||
|
|
||||||
const handleUseCurrentLocation = async () => {
|
const handleUseCurrentLocation = async () => {
|
||||||
|
|
@ -62,7 +66,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
|
||||||
loading={isGettingLocation || isRequestingLocation}
|
loading={isGettingLocation || isRequestingLocation}
|
||||||
className={styles.locationButton}
|
className={styles.locationButton}
|
||||||
>
|
>
|
||||||
Meine Position verwenden
|
{t('Meine Position verwenden')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { LogProps } from './LogTypes';
|
||||||
import { AutoScroll } from '../AutoScroll';
|
import { AutoScroll } from '../AutoScroll';
|
||||||
import { formatUnixTimestamp } from '../../../utils/time';
|
import { formatUnixTimestamp } from '../../../utils/time';
|
||||||
import styles from './Log.module.css';
|
import styles from './Log.module.css';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
// Helper to get status badge class
|
// Helper to get status badge class
|
||||||
const getStatusBadgeClass = (status?: string | null): string => {
|
const getStatusBadgeClass = (status?: string | null): string => {
|
||||||
|
|
@ -22,11 +23,13 @@ const getStatusBadgeClass = (status?: string | null): string => {
|
||||||
|
|
||||||
const Log: React.FC<LogProps> = ({
|
const Log: React.FC<LogProps> = ({
|
||||||
className = '',
|
className = '',
|
||||||
emptyMessage = 'No log information available',
|
emptyMessage = 'Keine Log-Informationen verfügbar',
|
||||||
dashboardTree,
|
dashboardTree,
|
||||||
onToggleOperationExpanded,
|
onToggleOperationExpanded,
|
||||||
getChildOperations
|
getChildOperations
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage;
|
||||||
const formatLogTimestamp = (timestamp: number): string => {
|
const formatLogTimestamp = (timestamp: number): string => {
|
||||||
try {
|
try {
|
||||||
const formatted = formatUnixTimestamp(timestamp, undefined, {
|
const formatted = formatUnixTimestamp(timestamp, undefined, {
|
||||||
|
|
@ -87,7 +90,7 @@ const Log: React.FC<LogProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use stable operation name (from first log) or fallback to operationId
|
// Use stable operation name (from first log) or fallback to operationId
|
||||||
const operationName = operation.operationName || `Operation ${operationId}`;
|
const operationName = operation.operationName || `${t('Operation')} ${operationId}`;
|
||||||
// Use latest message as status tag (updates with each poll)
|
// Use latest message as status tag (updates with each poll)
|
||||||
const latestMessage = operation.latestMessage || '';
|
const latestMessage = operation.latestMessage || '';
|
||||||
const operationStatus = operation.latestStatus || 'running';
|
const operationStatus = operation.latestStatus || 'running';
|
||||||
|
|
@ -137,7 +140,7 @@ const Log: React.FC<LogProps> = ({
|
||||||
<button
|
<button
|
||||||
className={styles.expandButton}
|
className={styles.expandButton}
|
||||||
onClick={() => onToggleOperationExpanded?.(operationId)}
|
onClick={() => onToggleOperationExpanded?.(operationId)}
|
||||||
aria-label={operation.expanded ? 'Collapse' : 'Expand'}
|
aria-label={operation.expanded ? t('Einklappen') : t('Ausklappen')}
|
||||||
>
|
>
|
||||||
<span className={`${styles.collapseIcon} ${operation.expanded ? '' : styles.collapsed}`}>
|
<span className={`${styles.collapseIcon} ${operation.expanded ? '' : styles.collapsed}`}>
|
||||||
▼
|
▼
|
||||||
|
|
@ -243,7 +246,7 @@ const Log: React.FC<LogProps> = ({
|
||||||
|
|
||||||
if (dashboardTree.rootOperations.length === 0) {
|
if (dashboardTree.rootOperations.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.emptyState}>{emptyMessage}</div>
|
<div className={styles.emptyState}>{resolvedEmptyMessage}</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,7 +263,7 @@ const Log: React.FC<LogProps> = ({
|
||||||
if (!hasDashboardLogs) {
|
if (!hasDashboardLogs) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.logContainer} ${className}`}>
|
<div className={`${styles.logContainer} ${className}`}>
|
||||||
<div className={styles.emptyState}>{emptyMessage}</div>
|
<div className={styles.emptyState}>{resolvedEmptyMessage}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
import { Message } from '../../Messages/MessagesTypes';
|
import { Message } from '../../Messages/MessagesTypes';
|
||||||
import { DocumentItem, MessageMetadata, ActionInfo } from '../../Messages/MessageParts';
|
import { DocumentItem, MessageMetadata, ActionInfo } from '../../Messages/MessageParts';
|
||||||
import { WorkflowFile } from '../../../../hooks/usePlayground';
|
import { WorkflowFile } from '../../../../hooks/usePlayground';
|
||||||
|
|
@ -41,6 +42,7 @@ export const LogMessage: React.FC<LogMessageProps> = ({
|
||||||
downloadingFiles,
|
downloadingFiles,
|
||||||
workflowId
|
workflowId
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
return (
|
return (
|
||||||
<div className={`${logStyles.logMessage} ${styles.messageWrapper}`}>
|
<div className={`${logStyles.logMessage} ${styles.messageWrapper}`}>
|
||||||
{/* Metadata row */}
|
{/* Metadata row */}
|
||||||
|
|
@ -58,7 +60,7 @@ export const LogMessage: React.FC<LogMessageProps> = ({
|
||||||
|
|
||||||
{message.summary && message.summary !== message.message && (
|
{message.summary && message.summary !== message.message && (
|
||||||
<div className={logStyles.logSummary}>
|
<div className={logStyles.logSummary}>
|
||||||
<strong>Summary:</strong> {message.summary}
|
<strong>{t('Zusammenfassung')}:</strong> {message.summary}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,9 +223,9 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
|
||||||
const polygon = L.polygon(latLngs, SELECTED_STYLE);
|
const polygon = L.polygon(latLngs, SELECTED_STYLE);
|
||||||
polygon.bindPopup(`
|
polygon.bindPopup(`
|
||||||
<div>
|
<div>
|
||||||
<strong>Parzelle ${parcel.number || parcel.id}</strong><br/>
|
<strong>${t('Parzelle')} ${parcel.number || parcel.id}</strong><br/>
|
||||||
${parcel.egrid ? `EGRID: ${parcel.egrid}<br/>` : ''}
|
${parcel.egrid ? `EGRID: ${parcel.egrid}<br/>` : ''}
|
||||||
<em>{t('Ausgewählt')}</em>
|
<em>${t('Ausgewählt')}</em>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
if (onParcelClick) {
|
if (onParcelClick) {
|
||||||
|
|
@ -235,7 +235,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
|
||||||
layersRef.current.push(polygon);
|
layersRef.current.push(polygon);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [parcels, combinedOutline, onParcelClick]);
|
}, [parcels, combinedOutline, onParcelClick, t]);
|
||||||
|
|
||||||
// Handle map clicks
|
// Handle map clicks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -361,7 +361,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
|
||||||
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
|
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
|
||||||
{parcels.length === 0 && !center && (
|
{parcels.length === 0 && !center && (
|
||||||
<div className={styles.emptyStateOverlay}>
|
<div className={styles.emptyStateOverlay}>
|
||||||
<p>{emptyMessage}</p>
|
<p>{typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showWfsParcels && isWfsLoading && (
|
{showWfsParcels && isWfsLoading && (
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
|
||||||
{/* Summary if different from message */}
|
{/* Summary if different from message */}
|
||||||
{message.summary && message.summary !== message.message && (
|
{message.summary && message.summary !== message.message && (
|
||||||
<div className={styles.messageSummary}>
|
<div className={styles.messageSummary}>
|
||||||
<strong>Summary:</strong> {message.summary}
|
<strong>{t('Zusammenfassung')}:</strong> {message.summary}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import { MessagesProps } from './MessagesTypes';
|
import { MessagesProps } from './MessagesTypes';
|
||||||
import { ChatMessage } from './ChatMessages/ChatMessage';
|
import { ChatMessage } from './ChatMessages/ChatMessage';
|
||||||
import { LogMessage } from '../Log/LogMessage/LogMessage';
|
import { LogMessage } from '../Log/LogMessage/LogMessage';
|
||||||
|
|
@ -15,7 +16,7 @@ const Messages: React.FC<MessagesProps> = ({
|
||||||
showProgress = true,
|
showProgress = true,
|
||||||
renderMessage,
|
renderMessage,
|
||||||
renderDocument,
|
renderDocument,
|
||||||
emptyMessage = 'No messages yet',
|
emptyMessage = 'Noch keine Nachrichten',
|
||||||
onFileDelete,
|
onFileDelete,
|
||||||
onFileRemove,
|
onFileRemove,
|
||||||
onFileView,
|
onFileView,
|
||||||
|
|
@ -28,10 +29,12 @@ const Messages: React.FC<MessagesProps> = ({
|
||||||
onMessageDelete,
|
onMessageDelete,
|
||||||
deletingMessages
|
deletingMessages
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage;
|
||||||
if (!messages || messages.length === 0) {
|
if (!messages || messages.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.messagesContainer} ${styles.emptyContainer} ${className}`}>
|
<div className={`${styles.messagesContainer} ${styles.emptyContainer} ${className}`}>
|
||||||
<div className={styles.emptyState}>{emptyMessage}</div>
|
<div className={styles.emptyState}>{resolvedEmptyMessage}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||||
import { FaChevronDown, FaChevronUp, FaFilePdf, FaInfoCircle } from 'react-icons/fa';
|
import { FaChevronDown, FaChevronUp, FaFilePdf, FaInfoCircle } from 'react-icons/fa';
|
||||||
import { UrlContentPreview } from '../../ContentPreview';
|
import { UrlContentPreview } from '../../ContentPreview';
|
||||||
import styles from './OerebSection.module.css';
|
import styles from './OerebSection.module.css';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export interface OerebData {
|
export interface OerebData {
|
||||||
extract_url?: string;
|
extract_url?: string;
|
||||||
|
|
@ -21,6 +22,7 @@ export interface OerebSectionProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
const restrictions = oereb.restrictions || [];
|
const restrictions = oereb.restrictions || [];
|
||||||
|
|
@ -34,7 +36,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
<div className={styles.oerebHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
<div className={styles.oerebHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
||||||
<h4 className={styles.subSectionTitle}>
|
<h4 className={styles.subSectionTitle}>
|
||||||
<FaInfoCircle style={{ marginRight: '8px', display: 'inline' }} />
|
<FaInfoCircle style={{ marginRight: '8px', display: 'inline' }} />
|
||||||
ÖREB-Kataster
|
{t('ÖREB-Kataster')}
|
||||||
{restrictions.length > 0 && (
|
{restrictions.length > 0 && (
|
||||||
<span className={styles.badge}>({restrictions.length})</span>
|
<span className={styles.badge}>({restrictions.length})</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -54,7 +56,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<FaFilePdf style={{ marginRight: '8px' }} />
|
<FaFilePdf style={{ marginRight: '8px' }} />
|
||||||
Vollständigen ÖREB-Auszug öffnen (PDF)
|
{t('Vollständigen ÖREB-Auszug öffnen (PDF)')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -68,14 +70,14 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
{restriction.law_status && (
|
{restriction.law_status && (
|
||||||
<span className={styles.restrictionStatus}>
|
<span className={styles.restrictionStatus}>
|
||||||
{restriction.law_status === 'inKraft' || restriction.law_status === 'inForce'
|
{restriction.law_status === 'inKraft' || restriction.law_status === 'inForce'
|
||||||
? 'In Kraft'
|
? t('In Kraft')
|
||||||
: restriction.law_status}
|
: restriction.law_status}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{restriction.type && (
|
{restriction.type && (
|
||||||
<div className={styles.restrictionType}>
|
<div className={styles.restrictionType}>
|
||||||
<span className={styles.label}>Typ:</span>
|
<span className={styles.label}>{t('Typ:')}</span>
|
||||||
<span className={styles.value}>{restriction.type}</span>
|
<span className={styles.value}>{restriction.type}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -86,7 +88,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
)}
|
)}
|
||||||
{restriction.documents && restriction.documents.length > 0 && (
|
{restriction.documents && restriction.documents.length > 0 && (
|
||||||
<div className={styles.restrictionDocuments}>
|
<div className={styles.restrictionDocuments}>
|
||||||
<span className={styles.label}>Dokumente:</span>
|
<span className={styles.label}>{t('Dokumente:')}</span>
|
||||||
{restriction.documents.map((doc, docIndex) => (
|
{restriction.documents.map((doc, docIndex) => (
|
||||||
<a
|
<a
|
||||||
key={docIndex}
|
key={docIndex}
|
||||||
|
|
@ -95,7 +97,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={styles.documentLink}
|
className={styles.documentLink}
|
||||||
>
|
>
|
||||||
Dokument {docIndex + 1}
|
{t('Dokument {nr}', { nr: String(docIndex + 1) })}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,14 +107,14 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.noRestrictions}>
|
<div className={styles.noRestrictions}>
|
||||||
Keine öffentlich-rechtlichen Beschränkungen gefunden.
|
{t('Keine öffentlich-rechtlichen Beschränkungen gefunden.')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{oereb.last_updated && (
|
{oereb.last_updated && (
|
||||||
<div className={styles.oerebFooter}>
|
<div className={styles.oerebFooter}>
|
||||||
<span className={styles.lastUpdated}>
|
<span className={styles.lastUpdated}>
|
||||||
Aktualisiert: {new Date(oereb.last_updated).toLocaleString('de-CH')}
|
{t('Aktualisiert')}: {new Date(oereb.last_updated).toLocaleString('de-CH')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -124,7 +126,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||||
isOpen={isPreviewOpen}
|
isOpen={isPreviewOpen}
|
||||||
onClose={() => setIsPreviewOpen(false)}
|
onClose={() => setIsPreviewOpen(false)}
|
||||||
url={oereb.extract_url}
|
url={oereb.extract_url}
|
||||||
fileName="ÖREB-Auszug.pdf"
|
fileName={t('ÖREB-Auszug.pdf')}
|
||||||
mimeType="application/pdf"
|
mimeType="application/pdf"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -147,12 +147,12 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setDocsError(prev => ({
|
setDocsError(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[parcelId]: e?.response?.data?.detail || e?.message || 'Fehler beim Laden'
|
[parcelId]: e?.response?.data?.detail || e?.message || t('Fehler beim Laden')
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
setDocsLoading(prev => ({ ...prev, [parcelId]: false }));
|
setDocsLoading(prev => ({ ...prev, [parcelId]: false }));
|
||||||
}
|
}
|
||||||
}, [instanceId]);
|
}, [instanceId, t]);
|
||||||
|
|
||||||
const runExtraction = useCallback(async (
|
const runExtraction = useCallback(async (
|
||||||
parcelId: string,
|
parcelId: string,
|
||||||
|
|
@ -174,12 +174,12 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setExtractError(prev => ({
|
setExtractError(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[parcelId]: e?.response?.data?.detail || e?.message || 'Fehler bei der Extraktion'
|
[parcelId]: e?.response?.data?.detail || e?.message || t('Fehler bei der Extraktion')
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
setExtractLoading(prev => ({ ...prev, [parcelId]: false }));
|
setExtractLoading(prev => ({ ...prev, [parcelId]: false }));
|
||||||
}
|
}
|
||||||
}, [instanceId]);
|
}, [instanceId, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || !instanceId) return;
|
if (!isOpen || !instanceId) return;
|
||||||
|
|
@ -218,7 +218,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
className={styles.panel}
|
className={styles.panel}
|
||||||
>
|
>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h2 className={styles.title}>Parzellen-Informationen ({parcels.length})</h2>
|
<h2 className={styles.title}>{t('Parzellen-Informationen')} ({parcels.length})</h2>
|
||||||
<button className={styles.closeButton} onClick={onClose}>
|
<button className={styles.closeButton} onClick={onClose}>
|
||||||
<FaTimes />
|
<FaTimes />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -248,7 +248,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
return [
|
return [
|
||||||
<section key={`h-${bz.bauzone}`} className={styles.bauzoneSection}>
|
<section key={`h-${bz.bauzone}`} className={styles.bauzoneSection}>
|
||||||
<h4 className={styles.bauzoneTitle}>
|
<h4 className={styles.bauzoneTitle}>
|
||||||
Bauzone {bz.bauzone}
|
{t('Bauzone')} {bz.bauzone}
|
||||||
{bz.area_m2 != null && (
|
{bz.area_m2 != null && (
|
||||||
<span className={styles.bauzoneArea}> — {bz.area_m2.toFixed(2)} m²</span>
|
<span className={styles.bauzoneArea}> — {bz.area_m2.toFixed(2)} m²</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -260,7 +260,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
<section key={parcelData.parcel.id} className={styles.section}>
|
<section key={parcelData.parcel.id} className={styles.section}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<h3 className={styles.sectionTitle}>
|
<h3 className={styles.sectionTitle}>
|
||||||
Parzelle {idx + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'}
|
{t('Parzelle')} {idx + 1}: {parcelData.parcel.number || parcelData.parcel.id || t('Unbekannt')}
|
||||||
</h3>
|
</h3>
|
||||||
{onRemoveParcel && (
|
{onRemoveParcel && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -342,7 +342,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
)}
|
)}
|
||||||
{instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
|
{instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
|
||||||
<div className={styles.bzoSection}>
|
<div className={styles.bzoSection}>
|
||||||
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
|
<h4 className={styles.subSectionTitle}>{t('Bauzonenverordnung')}</h4>
|
||||||
{docsLoading[parcelData.parcel.id] && (
|
{docsLoading[parcelData.parcel.id] && (
|
||||||
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
|
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -364,7 +364,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
})}
|
})}
|
||||||
title={t('Dokument öffnen')}
|
title={t('Dokument öffnen')}
|
||||||
>
|
>
|
||||||
<FaEye /> Öffnen
|
<FaEye /> {t('Öffnen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -387,7 +387,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
) : (
|
) : (
|
||||||
<FaFileAlt />
|
<FaFileAlt />
|
||||||
)}
|
)}
|
||||||
Inhalt extrahieren (LangGraph)
|
{t('Inhalt extrahieren (LangGraph)')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{extractError[parcelData.parcel.id] && (
|
{extractError[parcelData.parcel.id] && (
|
||||||
|
|
@ -470,7 +470,9 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
<div className={styles.infoItem}>
|
<div className={styles.infoItem}>
|
||||||
<span className={styles.label}>Zone:</span>
|
<span className={styles.label}>Zone:</span>
|
||||||
<span className={styles.value}>
|
<span className={styles.value}>
|
||||||
{parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden
|
{parcelData.parcel.zone.length !== 1
|
||||||
|
? t('{n} Zonen gefunden', { n: String(parcelData.parcel.zone.length) })
|
||||||
|
: t('1 Zone gefunden')}
|
||||||
{(() => {
|
{(() => {
|
||||||
// Extract zone types from zone array
|
// Extract zone types from zone array
|
||||||
const zoneTypes = parcelData.parcel.zone
|
const zoneTypes = parcelData.parcel.zone
|
||||||
|
|
@ -509,7 +511,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
>
|
>
|
||||||
Link öffnen
|
{t('Link öffnen')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -526,7 +528,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
<section key={parcelData.parcel.id || index} className={styles.section}>
|
<section key={parcelData.parcel.id || index} className={styles.section}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<h3 className={styles.sectionTitle}>
|
<h3 className={styles.sectionTitle}>
|
||||||
Parzelle {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'}
|
{t('Parzelle')} {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || t('Unbekannt')}
|
||||||
</h3>
|
</h3>
|
||||||
{onRemoveParcel && (
|
{onRemoveParcel && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -608,7 +610,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
)}
|
)}
|
||||||
{instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
|
{instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
|
||||||
<div className={styles.bzoSection}>
|
<div className={styles.bzoSection}>
|
||||||
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
|
<h4 className={styles.subSectionTitle}>{t('Bauzonenverordnung')}</h4>
|
||||||
{docsLoading[parcelData.parcel.id] && (
|
{docsLoading[parcelData.parcel.id] && (
|
||||||
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
|
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -630,7 +632,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
})}
|
})}
|
||||||
title={t('Dokument öffnen')}
|
title={t('Dokument öffnen')}
|
||||||
>
|
>
|
||||||
<FaEye /> Öffnen
|
<FaEye /> {t('Öffnen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -653,7 +655,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
) : (
|
) : (
|
||||||
<FaFileAlt />
|
<FaFileAlt />
|
||||||
)}
|
)}
|
||||||
Inhalt extrahieren (LangGraph)
|
{t('Inhalt extrahieren (LangGraph)')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{extractError[parcelData.parcel.id] && (
|
{extractError[parcelData.parcel.id] && (
|
||||||
|
|
@ -736,7 +738,9 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
<div className={styles.infoItem}>
|
<div className={styles.infoItem}>
|
||||||
<span className={styles.label}>Zone:</span>
|
<span className={styles.label}>Zone:</span>
|
||||||
<span className={styles.value}>
|
<span className={styles.value}>
|
||||||
{parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden
|
{parcelData.parcel.zone.length !== 1
|
||||||
|
? t('{n} Zonen gefunden', { n: String(parcelData.parcel.zone.length) })
|
||||||
|
: t('1 Zone gefunden')}
|
||||||
{(() => {
|
{(() => {
|
||||||
const zoneTypes = parcelData.parcel.zone
|
const zoneTypes = parcelData.parcel.zone
|
||||||
.map((z: any) => {
|
.map((z: any) => {
|
||||||
|
|
@ -773,7 +777,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
>
|
>
|
||||||
Link öffnen
|
{t('Link öffnen')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,17 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
||||||
latestStats
|
latestStats
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const statusLabel = (status: WorkflowStatusType | null): string => {
|
||||||
|
if (!status) return '';
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
completed: t('Abgeschlossen'),
|
||||||
|
failed: t('Fehlgeschlagen'),
|
||||||
|
started: t('Gestartet'),
|
||||||
|
stopped: t('Gestoppt'),
|
||||||
|
resumed: t('Fortgesetzt'),
|
||||||
|
};
|
||||||
|
return map[status] ?? status;
|
||||||
|
};
|
||||||
// Use workflow status and round from API response, fallback to extracting from logs
|
// Use workflow status and round from API response, fallback to extracting from logs
|
||||||
const workflowStatus = useMemo(() => {
|
const workflowStatus = useMemo(() => {
|
||||||
if (workflowStatusFromApi) {
|
if (workflowStatusFromApi) {
|
||||||
|
|
@ -73,11 +84,11 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
||||||
)}
|
)}
|
||||||
{workflowStatus.status && (
|
{workflowStatus.status && (
|
||||||
<span className={styles.statusBadge} data-status={workflowStatus.status}>
|
<span className={styles.statusBadge} data-status={workflowStatus.status}>
|
||||||
{workflowStatus.status.charAt(0).toUpperCase() + workflowStatus.status.slice(1)}
|
{statusLabel(workflowStatus.status)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{workflowStatus.round !== null && (
|
{workflowStatus.round !== null && (
|
||||||
<span className={styles.roundBadge}>Round {workflowStatus.round}</span>
|
<span className={styles.roundBadge}>{t('Runde {nr}', { nr: String(workflowStatus.round) })}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -85,7 +96,7 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
||||||
{latestStats && latestStats.priceCHF !== undefined && (
|
{latestStats && latestStats.priceCHF !== undefined && (
|
||||||
<div className={styles.statsContainer}>
|
<div className={styles.statsContainer}>
|
||||||
<div className={styles.statItem}>
|
<div className={styles.statItem}>
|
||||||
<span className={styles.statLabel}>Cost:</span>
|
<span className={styles.statLabel}>{t('Kosten')}</span>
|
||||||
<span className={styles.statValue}>{_formatCurrency(latestStats.priceCHF)}</span>
|
<span className={styles.statValue}>{_formatCurrency(latestStats.priceCHF)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
}
|
}
|
||||||
groupMap.get(fiId)!.chats.push({
|
groupMap.get(fiId)!.chats.push({
|
||||||
id: wf.id,
|
id: wf.id,
|
||||||
label: wf.label || wf.name || `Chat ${wf.id.slice(0, 8)}`,
|
label: wf.label || wf.name || `${t('Chat')} ${wf.id.slice(0, 8)}`,
|
||||||
updatedAt: wf.updatedAt || wf.lastActivity || wf.startedAt,
|
updatedAt: wf.updatedAt || wf.lastActivity || wf.startedAt,
|
||||||
lastMessageAt: wf.lastMessageAt,
|
lastMessageAt: wf.lastMessageAt,
|
||||||
featureInstanceId: fiId,
|
featureInstanceId: fiId,
|
||||||
|
|
@ -132,7 +132,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [context.instanceId]);
|
}, [context.instanceId, t]);
|
||||||
|
|
||||||
useEffect(() => { _loadChats(); }, [_loadChats]);
|
useEffect(() => { _loadChats(); }, [_loadChats]);
|
||||||
|
|
||||||
|
|
@ -283,7 +283,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
<button
|
<button
|
||||||
className={styles.actionBtn}
|
className={styles.actionBtn}
|
||||||
onClick={(e) => { e.stopPropagation(); _startEditing(chat); }}
|
onClick={(e) => { e.stopPropagation(); _startEditing(chat); }}
|
||||||
title="Umbenennen"
|
title={t('Umbenennen')}
|
||||||
>
|
>
|
||||||
✏️
|
✏️
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -292,7 +292,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
<button
|
<button
|
||||||
className={styles.actionBtn}
|
className={styles.actionBtn}
|
||||||
onClick={(e) => { e.stopPropagation(); _restoreChat(chat.id); }}
|
onClick={(e) => { e.stopPropagation(); _restoreChat(chat.id); }}
|
||||||
title="Wiederherstellen"
|
title={t('Wiederherstellen')}
|
||||||
>
|
>
|
||||||
↩️
|
↩️
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -300,7 +300,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
<button
|
<button
|
||||||
className={styles.actionBtn}
|
className={styles.actionBtn}
|
||||||
onClick={(e) => { e.stopPropagation(); _archiveChat(chat.id); }}
|
onClick={(e) => { e.stopPropagation(); _archiveChat(chat.id); }}
|
||||||
title="Archivieren"
|
title={t('Archivieren')}
|
||||||
>
|
>
|
||||||
📦
|
📦
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -323,10 +323,10 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
|
|
||||||
const _featureCodeLabel = (code: string): string => {
|
const _featureCodeLabel = (code: string): string => {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
workspace: 'AI Workspace',
|
workspace: t('KI-Arbeitsbereich'),
|
||||||
commcoach: 'CommCoach',
|
commcoach: t('CommCoach'),
|
||||||
trustee: 'Trustee',
|
trustee: t('Trustee'),
|
||||||
automation: 'Automation',
|
automation: t('Automation'),
|
||||||
};
|
};
|
||||||
return labels[code] || code;
|
return labels[code] || code;
|
||||||
};
|
};
|
||||||
|
|
@ -351,7 +351,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
<button
|
<button
|
||||||
className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`}
|
className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`}
|
||||||
onClick={() => setFlatMode(!flatMode)}
|
onClick={() => setFlatMode(!flatMode)}
|
||||||
title={flatMode ? 'Baumansicht' : 'Listenansicht'}
|
title={flatMode ? t('Baumansicht') : t('Listenansicht')}
|
||||||
>
|
>
|
||||||
{flatMode ? '\uD83C\uDF33' : '\uD83D\uDCCB'}
|
{flatMode ? '\uD83C\uDF33' : '\uD83D\uDCCB'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -362,13 +362,13 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
className={`${styles.filterTab} ${filter === 'active' ? styles.filterTabActive : ''}`}
|
className={`${styles.filterTab} ${filter === 'active' ? styles.filterTabActive : ''}`}
|
||||||
onClick={() => setFilter('active')}
|
onClick={() => setFilter('active')}
|
||||||
>
|
>
|
||||||
Aktiv ({_activeCount})
|
{t('Aktiv')} ({_activeCount})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${styles.filterTab} ${filter === 'archived' ? styles.filterTabActive : ''}`}
|
className={`${styles.filterTab} ${filter === 'archived' ? styles.filterTabActive : ''}`}
|
||||||
onClick={() => setFilter('archived')}
|
onClick={() => setFilter('archived')}
|
||||||
>
|
>
|
||||||
Archiv ({_archivedCount})
|
{t('Archiv')} ({_archivedCount})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -250,12 +250,12 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
fontSize: 13, fontWeight: 600, color: '#F25843',
|
fontSize: 13, fontWeight: 600, color: '#F25843',
|
||||||
}}>
|
}}>
|
||||||
Dateien hier ablegen
|
{t('Dateien hier ablegen')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px' }}>
|
||||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>Files</span>
|
<span style={{ fontSize: 12, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>{t('Dateien')}</span>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
|
@ -331,10 +331,10 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.legend}>
|
<div className={styles.legend}>
|
||||||
<span>{'\uD83D\uDC64'} Persönlich</span>
|
<span>{'\uD83D\uDC64'} {t('Persönlich')}</span>
|
||||||
<span>{'\uD83D\uDC65'} Instanz</span>
|
<span>{'\uD83D\uDC65'} {t('Instanz')}</span>
|
||||||
<span>{'\uD83C\uDFE2'} Mandant</span>
|
<span>{'\uD83C\uDFE2'} {t('Mandant')}</span>
|
||||||
<span>{'\uD83D\uDD12'} Neutralisiert</span>
|
<span>{'\uD83D\uDD12'} {t('Neutralisiert')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ interface MandateGroupNode {
|
||||||
interface FeatureTableNode {
|
interface FeatureTableNode {
|
||||||
objectKey: string;
|
objectKey: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
label: Record<string, string>;
|
label: string;
|
||||||
fields: string[];
|
fields: string[];
|
||||||
isParent?: boolean;
|
isParent?: boolean;
|
||||||
parentTable?: string;
|
parentTable?: string;
|
||||||
|
|
@ -185,13 +185,6 @@ const _SCOPE_ICONS: Record<string, string> = {
|
||||||
global: '\uD83C\uDF10',
|
global: '\uD83C\uDF10',
|
||||||
};
|
};
|
||||||
|
|
||||||
const _SCOPE_LABELS: Record<string, string> = {
|
|
||||||
personal: 'Personal',
|
|
||||||
featureInstance: 'Feature Instance',
|
|
||||||
mandate: 'Mandate',
|
|
||||||
global: 'Global',
|
|
||||||
};
|
|
||||||
|
|
||||||
function _nextScope(current: string): string {
|
function _nextScope(current: string): string {
|
||||||
const idx = _SCOPE_ORDER.indexOf(current);
|
const idx = _SCOPE_ORDER.indexOf(current);
|
||||||
if (idx === -1) return _SCOPE_ORDER[0];
|
if (idx === -1) return _SCOPE_ORDER[0];
|
||||||
|
|
@ -348,6 +341,14 @@ function _Spinner(): React.ReactElement {
|
||||||
|
|
||||||
const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
|
const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const _scopeLabel = (scope: string) => ({
|
||||||
|
personal: t('Persönlich'),
|
||||||
|
featureInstance: t('Feature-Instanz'),
|
||||||
|
mandate: t('Mandant'),
|
||||||
|
global: t('Global'),
|
||||||
|
} as Record<string, string>)[scope] || scope;
|
||||||
|
const _scopeCycleTitle = (scope: string) =>
|
||||||
|
`${t('Bereich')}: ${_scopeLabel(scope)} → ${_scopeLabel(_nextScope(scope))}`;
|
||||||
const instanceId = context.instanceId;
|
const instanceId = context.instanceId;
|
||||||
|
|
||||||
/* ── Active sources (fetched internally) ── */
|
/* ── Active sources (fetched internally) ── */
|
||||||
|
|
@ -663,7 +664,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
featureCode: node.featureCode,
|
featureCode: node.featureCode,
|
||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
objectKey: table.objectKey,
|
objectKey: table.objectKey,
|
||||||
label: table.label?.en || table.label?.de || table.tableName,
|
label: table.label || table.tableName,
|
||||||
});
|
});
|
||||||
_fetchFeatureDataSources();
|
_fetchFeatureDataSources();
|
||||||
onSourcesChanged?.();
|
onSourcesChanged?.();
|
||||||
|
|
@ -764,7 +765,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName);
|
const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName);
|
||||||
|
|
||||||
if (parentTable) {
|
if (parentTable) {
|
||||||
const parentLabel = `${parentTable.label?.en || parentTable.label?.de || parentTable.tableName}: ${parentRecord.displayLabel}`;
|
const parentLabel = `${parentTable.label || parentTable.tableName}: ${parentRecord.displayLabel}`;
|
||||||
await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
|
await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
|
||||||
featureInstanceId: node.featureInstanceId,
|
featureInstanceId: node.featureInstanceId,
|
||||||
featureCode: node.featureCode,
|
featureCode: node.featureCode,
|
||||||
|
|
@ -776,7 +777,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of childTables) {
|
for (const child of childTables) {
|
||||||
const childLabel = `${child.label?.en || child.label?.de || child.tableName}: ${parentRecord.displayLabel}`;
|
const childLabel = `${child.label || child.tableName}: ${parentRecord.displayLabel}`;
|
||||||
await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
|
await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
|
||||||
featureInstanceId: node.featureInstanceId,
|
featureInstanceId: node.featureInstanceId,
|
||||||
featureCode: node.featureCode,
|
featureCode: node.featureCode,
|
||||||
|
|
@ -813,7 +814,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
{dataSources.length > 0 && (
|
{dataSources.length > 0 && (
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
||||||
Active Personal Sources
|
{t('Aktive persönliche Quellen')}
|
||||||
</div>
|
</div>
|
||||||
{[...dataSources].sort((a, b) => {
|
{[...dataSources].sort((a, b) => {
|
||||||
const aKey = `${a.sourceType}|${a.label || a.path || ''}`;
|
const aKey = `${a.sourceType}|${a.label || a.path || ''}`;
|
||||||
|
|
@ -842,7 +843,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
background: 'none', border: 'none', cursor: 'pointer',
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
fontSize: 13, padding: '0 2px', lineHeight: 1,
|
fontSize: 13, padding: '0 2px', lineHeight: 1,
|
||||||
}}
|
}}
|
||||||
title={`Scope: ${_SCOPE_LABELS[ds.scope] || ds.scope} → ${_SCOPE_LABELS[_nextScope(ds.scope)]}`}
|
title={_scopeCycleTitle(ds.scope)}
|
||||||
>
|
>
|
||||||
{_SCOPE_ICONS[ds.scope] || _SCOPE_ICONS.personal}
|
{_SCOPE_ICONS[ds.scope] || _SCOPE_ICONS.personal}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -874,7 +875,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
{/* ── Browse Sources header ── */}
|
{/* ── Browse Sources header ── */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||||
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
|
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
|
||||||
Browse Sources
|
{t('Quellen durchsuchen')}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={_loadConnections}
|
onClick={_loadConnections}
|
||||||
|
|
@ -888,13 +889,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
{/* ── Browse Sources tree ── */}
|
{/* ── Browse Sources tree ── */}
|
||||||
{loadingRoot && tree.length === 0 && (
|
{loadingRoot && tree.length === 0 && (
|
||||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||||
Loading connections...
|
{t('Verbindungen werden geladen…')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loadingRoot && tree.length === 0 && (
|
{!loadingRoot && tree.length === 0 && (
|
||||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||||
No active connections found.
|
{t('Keine aktiven Verbindungen.')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -917,7 +918,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
{featureDataSources.length > 0 && (
|
{featureDataSources.length > 0 && (
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
||||||
Active Feature Sources
|
{t('Aktive Feature-Quellen')}
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const sorted = [...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || ''));
|
const sorted = [...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || ''));
|
||||||
|
|
@ -979,7 +980,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
<button
|
<button
|
||||||
onClick={() => _cycleFeatureScope(fds)}
|
onClick={() => _cycleFeatureScope(fds)}
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1 }}
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1 }}
|
||||||
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope} → ${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
|
title={_scopeCycleTitle(fds.scope)}
|
||||||
>
|
>
|
||||||
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1022,7 +1023,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
<button
|
<button
|
||||||
onClick={() => _cycleFeatureScope(fds)}
|
onClick={() => _cycleFeatureScope(fds)}
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1 }}
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1 }}
|
||||||
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope} → ${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
|
title={_scopeCycleTitle(fds.scope)}
|
||||||
>
|
>
|
||||||
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1053,7 +1054,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
{/* ── Feature Data header ── */}
|
{/* ── Feature Data header ── */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||||
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
|
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
|
||||||
Feature Data
|
{t('Feature-Daten')}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={_loadFeatureConnections}
|
onClick={_loadFeatureConnections}
|
||||||
|
|
@ -1067,13 +1068,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
{/* ── Feature Data tree ── */}
|
{/* ── Feature Data tree ── */}
|
||||||
{loadingFeatures && featureTree.length === 0 && (
|
{loadingFeatures && featureTree.length === 0 && (
|
||||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||||
Loading feature instances...
|
{t('Feature-Instanzen werden geladen…')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loadingFeatures && featureTree.length === 0 && (
|
{!loadingFeatures && featureTree.length === 0 && (
|
||||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||||
No feature instances found.
|
{t('Keine Feature-Instanzen gefunden.')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1169,7 +1170,7 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
|
||||||
}}
|
}}
|
||||||
title={t('Als Datenquelle hinzufügen')}
|
title={t('Als Datenquelle hinzufügen')}
|
||||||
>
|
>
|
||||||
{isAdding ? '...' : '+ Add'}
|
{isAdding ? '...' : `+ ${t('Hinzufügen')}`}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{canAdd && alreadyAdded && (
|
{canAdd && alreadyAdded && (
|
||||||
|
|
@ -1197,7 +1198,7 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
|
||||||
|
|
||||||
{node.expanded && node.children && node.children.length === 0 && !node.loading && (
|
{node.expanded && node.children && node.children.length === 0 && !node.loading && (
|
||||||
<div style={{ paddingLeft: (depth + 1) * 16 + 20, fontSize: 11, color: '#bbb', padding: '2px 0 2px ' + ((depth + 1) * 16 + 20) + 'px' }}>
|
<div style={{ paddingLeft: (depth + 1) * 16 + 20, fontSize: 11, color: '#bbb', padding: '2px 0 2px ' + ((depth + 1) * 16 + 20) + 'px' }}>
|
||||||
(empty)
|
{t('(leer)')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1299,6 +1300,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
||||||
onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
|
onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
|
||||||
expandedParentGroups, loadingParentGroup, addingParentKey,
|
expandedParentGroups, loadingParentGroup, addingParentKey,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const chevron = node.expanded ? '\u25BE' : '\u25B8';
|
const chevron = node.expanded ? '\u25BE' : '\u25B8';
|
||||||
|
|
||||||
|
|
@ -1329,7 +1331,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
||||||
{node.label}
|
{node.label}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
|
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
|
||||||
{node.tableCount} tables
|
{node.tableCount} {t('Tabellen')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1342,7 +1344,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
||||||
const isGroupLoading = loadingParentGroup === groupKey;
|
const isGroupLoading = loadingParentGroup === groupKey;
|
||||||
const records = node.parentRecords[pt.tableName];
|
const records = node.parentRecords[pt.tableName];
|
||||||
const childTables = (node.tables || []).filter(t => t.parentTable === pt.tableName);
|
const childTables = (node.tables || []).filter(t => t.parentTable === pt.tableName);
|
||||||
const ptLabel = pt.label?.en || pt.label?.de || pt.tableName;
|
const ptLabel = pt.label || pt.tableName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<_ParentGroupView
|
<_ParentGroupView
|
||||||
|
|
@ -1380,7 +1382,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
||||||
|
|
||||||
{node.expanded && node.tables && node.tables.length === 0 && !node.loading && (
|
{node.expanded && node.tables && node.tables.length === 0 && !node.loading && (
|
||||||
<div style={{ paddingLeft: 36, fontSize: 11, color: '#bbb', padding: '2px 0 2px 36px' }}>
|
<div style={{ paddingLeft: 36, fontSize: 11, color: '#bbb', padding: '2px 0 2px 36px' }}>
|
||||||
(no tables)
|
{t('(keine Tabellen)')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1402,7 +1404,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const tableLabel = table.label?.en || table.label?.de || table.tableName;
|
const tableLabel = table.label || table.tableName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -1433,7 +1435,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
|
||||||
}}
|
}}
|
||||||
title={t('Als Feature-Datenquelle hinzufügen')}
|
title={t('Als Feature-Datenquelle hinzufügen')}
|
||||||
>
|
>
|
||||||
{isAdding ? '...' : '+ Add'}
|
{isAdding ? '...' : `+ ${t('Hinzufügen')}`}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{isAdded && (
|
{isAdded && (
|
||||||
|
|
@ -1467,6 +1469,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
|
||||||
featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables,
|
featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables,
|
||||||
onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey,
|
onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const chevron = expanded ? '\u25BE' : '\u25B8';
|
const chevron = expanded ? '\u25BE' : '\u25B8';
|
||||||
|
|
||||||
|
|
@ -1493,7 +1496,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
|
||||||
</span>
|
</span>
|
||||||
{childTables.length > 0 && (
|
{childTables.length > 0 && (
|
||||||
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
|
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
|
||||||
+{childTables.length} tables
|
+{childTables.length} {t('Tabellen')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1518,7 +1521,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
|
||||||
|
|
||||||
{expanded && records && records.length === 0 && !loading && (
|
{expanded && records && records.length === 0 && !loading && (
|
||||||
<div style={{ paddingLeft: 52, fontSize: 11, color: '#bbb', padding: '2px 0 2px 52px' }}>
|
<div style={{ paddingLeft: 52, fontSize: 11, color: '#bbb', padding: '2px 0 2px 52px' }}>
|
||||||
(no records)
|
{t('(keine Einträge)')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1580,7 +1583,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
|
||||||
}}
|
}}
|
||||||
title={t('Alle Tabellen für diese Quelle hinzufügen')}
|
title={t('Alle Tabellen für diese Quelle hinzufügen')}
|
||||||
>
|
>
|
||||||
{isAdding ? '...' : '+ Add'}
|
{isAdding ? '...' : `+ ${t('Hinzufügen')}`}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{isAdded && (
|
{isAdded && (
|
||||||
|
|
@ -1593,7 +1596,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
|
||||||
{record.expanded && (
|
{record.expanded && (
|
||||||
<div style={{ paddingLeft: 64 }}>
|
<div style={{ paddingLeft: 64 }}>
|
||||||
{childTables.map(ct => {
|
{childTables.map(ct => {
|
||||||
const ctLabel = ct.label?.en || ct.label?.de || ct.tableName;
|
const ctLabel = ct.label || ct.tableName;
|
||||||
return (
|
return (
|
||||||
<div key={ct.objectKey} style={{
|
<div key={ct.objectKey} style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 4,
|
display: 'flex', alignItems: 'center', gap: 4,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ interface UnifiedDataBarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const _TAB_KEYS: Record<UdbTab, string> = {
|
const _TAB_KEYS: Record<UdbTab, string> = {
|
||||||
chats: 'Chats',
|
chats: 'Chatverläufe',
|
||||||
files: 'Dateien',
|
files: 'Dateien',
|
||||||
sources: 'Quellen',
|
sources: 'Quellen',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,6 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
|
|
||||||
// Helper function to resolve node name
|
// Helper function to resolve node name
|
||||||
const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => {
|
const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => {
|
||||||
const { t } = useLanguage();
|
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
return resolveLanguageText(page.name, t);
|
return resolveLanguageText(page.name, t);
|
||||||
}
|
}
|
||||||
|
|
@ -460,7 +458,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
setSidebarItems(items);
|
setSidebarItems(items);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ SidebarProvider: Error refreshing sidebar:', err);
|
console.error('❌ SidebarProvider: Error refreshing sidebar:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load sidebar items');
|
setError(err instanceof Error ? err.message : t('Seitenleiste konnte nicht geladen werden'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,11 @@ export interface GenericPageData {
|
||||||
type TranslationFunction = (key: string, fallback?: string) => string;
|
type TranslationFunction = (key: string, fallback?: string) => string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve display text from a page name that may be a i18n key or { de?, en? }.
|
* Resolve display text from a page name (i18n key) via the translation function.
|
||||||
*/
|
*/
|
||||||
export function resolveLanguageText(
|
export function resolveLanguageText(
|
||||||
name: string | { de?: string; en?: string },
|
name: string,
|
||||||
t: TranslationFunction
|
t: TranslationFunction
|
||||||
): string {
|
): string {
|
||||||
if (typeof name === 'string') {
|
return t(name);
|
||||||
const resolved = t(name);
|
|
||||||
return resolved !== name ? resolved : name;
|
|
||||||
}
|
|
||||||
const lang = (typeof navigator !== 'undefined' && navigator.language?.startsWith('en')) ? 'en' : 'de';
|
|
||||||
return name[lang] ?? name.de ?? name.en ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { useLanguage } from '../providers/language/LanguageContext';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
|
|
@ -78,6 +79,7 @@ interface SaveResult {
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac', mandateId?: string) {
|
export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac', mandateId?: string) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [rules, setRules] = useState<AccessRule[]>([]);
|
const [rules, setRules] = useState<AccessRule[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
@ -113,14 +115,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac'
|
||||||
setRules(fetchedRules);
|
setRules(fetchedRules);
|
||||||
return fetchedRules;
|
return fetchedRules;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden der Regeln';
|
const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Laden der Regeln');
|
||||||
setError(errorMsg);
|
setError(errorMsg);
|
||||||
console.error('Error fetching rules:', err);
|
console.error('Error fetching rules:', err);
|
||||||
return [];
|
return [];
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [roleId, apiBasePath, isInstanceApi, getHeaders]);
|
}, [roleId, apiBasePath, isInstanceApi, getHeaders, t]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save all rules for the role
|
* Save all rules for the role
|
||||||
|
|
@ -196,14 +198,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac'
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Speichern';
|
const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Speichern');
|
||||||
setError(errorMsg);
|
setError(errorMsg);
|
||||||
console.error('Error saving rules:', err);
|
console.error('Error saving rules:', err);
|
||||||
return { success: false, error: errorMsg };
|
return { success: false, error: errorMsg };
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders]);
|
}, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders, t]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get rules grouped by context
|
* Get rules grouped by context
|
||||||
|
|
|
||||||
|
|
@ -188,15 +188,10 @@ export function useMandates() {
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
|
|
@ -206,15 +201,10 @@ export function useMandates() {
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
|
|
@ -327,15 +317,10 @@ export function useMandates() {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
}
|
}
|
||||||
|
|
@ -343,15 +328,10 @@ export function useMandates() {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,15 +229,10 @@ export function useRbacRoles() {
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
|
|
@ -247,15 +242,10 @@ export function useRbacRoles() {
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
|
|
@ -368,15 +358,10 @@ export function useRbacRoles() {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
}
|
}
|
||||||
|
|
@ -384,15 +369,10 @@ export function useRbacRoles() {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,15 +205,10 @@ export function useRbacRules() {
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
|
|
@ -223,15 +218,10 @@ export function useRbacRules() {
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
|
|
@ -344,15 +334,10 @@ export function useRbacRules() {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
}
|
}
|
||||||
|
|
@ -360,15 +345,10 @@ export function useRbacRules() {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
const attrOptions = (attr as any).options;
|
const attrOptions = (attr as any).options;
|
||||||
if (Array.isArray(attrOptions)) {
|
if (Array.isArray(attrOptions)) {
|
||||||
options = attrOptions.map((opt: any) => {
|
options = attrOptions.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attrOptions === 'string') {
|
} else if (typeof attrOptions === 'string') {
|
||||||
optionsReference = attrOptions;
|
optionsReference = attrOptions;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export interface PaginationMetadata {
|
||||||
|
|
||||||
export interface Feature {
|
export interface Feature {
|
||||||
code: string;
|
code: string;
|
||||||
label: string | { [key: string]: string };
|
label: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ export interface FeatureAccessUser {
|
||||||
export interface FeatureInstanceRole {
|
export interface FeatureInstanceRole {
|
||||||
id: string;
|
id: string;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
description?: { [key: string]: string };
|
description?: string;
|
||||||
featureCode?: string;
|
featureCode?: string;
|
||||||
isSystemRole?: boolean;
|
isSystemRole?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -312,7 +312,7 @@ export function useFeatureAccess() {
|
||||||
name: string;
|
name: string;
|
||||||
features: Array<{
|
features: Array<{
|
||||||
code: string;
|
code: string;
|
||||||
label: string | { [key: string]: string };
|
label: string;
|
||||||
instances: Array<{
|
instances: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
featureCode: string;
|
featureCode: string;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import api from '../api';
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: string;
|
id: string;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
description?: string | { [key: string]: string };
|
description?: string;
|
||||||
mandateId?: string;
|
mandateId?: string;
|
||||||
featureInstanceId?: string;
|
featureInstanceId?: string;
|
||||||
featureCode?: string;
|
featureCode?: string;
|
||||||
|
|
@ -24,7 +24,7 @@ export interface Role {
|
||||||
|
|
||||||
export interface RoleCreate {
|
export interface RoleCreate {
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
description?: string | { [key: string]: string };
|
description?: string;
|
||||||
mandateId?: string;
|
mandateId?: string;
|
||||||
featureInstanceId?: string;
|
featureInstanceId?: string;
|
||||||
featureCode?: string;
|
featureCode?: string;
|
||||||
|
|
@ -32,7 +32,7 @@ export interface RoleCreate {
|
||||||
|
|
||||||
export interface RoleUpdate {
|
export interface RoleUpdate {
|
||||||
roleLabel?: string;
|
roleLabel?: string;
|
||||||
description?: string | { [key: string]: string };
|
description?: string;
|
||||||
mandateId?: string | null;
|
mandateId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export interface AttributeDefinition {
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
default?: any;
|
default?: any;
|
||||||
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
|
options?: Array<{ value: string | number; label: string }> | string;
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export type { Prompt, AttributeDefinition, PaginationParams };
|
||||||
// Re-export AttributeOption for backward compatibility
|
// Re-export AttributeOption for backward compatibility
|
||||||
export interface AttributeOption {
|
export interface AttributeOption {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string | { [key: string]: string };
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompts list hook
|
// Prompts list hook
|
||||||
|
|
@ -185,15 +185,10 @@ export function usePrompts() {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: String(opt.label ?? opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
@ -201,15 +196,10 @@ export function usePrompts() {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: String(opt.label ?? opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export interface RbacExportScope {
|
||||||
|
|
||||||
export interface RbacExportRole {
|
export interface RbacExportRole {
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
description?: { [key: string]: string };
|
description?: string;
|
||||||
featureCode?: string;
|
featureCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = (attr.options as any[]).map((opt: any) => ({
|
options = (attr.options as any[]).map((opt: any) => ({
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
label: opt.label || String(opt.value),
|
||||||
}));
|
}));
|
||||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||||
} else if (attr.type === 'multiselect') {
|
} else if (attr.type === 'multiselect') {
|
||||||
|
|
@ -189,7 +189,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = (attr.options as any[]).map((opt: any) => ({
|
options = (attr.options as any[]).map((opt: any) => ({
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
label: opt.label || String(opt.value),
|
||||||
}));
|
}));
|
||||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||||
} else if (attr.type === 'textarea') fieldType = 'textarea';
|
} else if (attr.type === 'textarea') fieldType = 'textarea';
|
||||||
|
|
@ -224,7 +224,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = (attr.options as any[]).map((opt: any) => ({
|
options = (attr.options as any[]).map((opt: any) => ({
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
label: opt.label || String(opt.value),
|
||||||
}));
|
}));
|
||||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||||
} else if (attr.type === 'multiselect') {
|
} else if (attr.type === 'multiselect') {
|
||||||
|
|
@ -232,7 +232,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = (attr.options as any[]).map((opt: any) => ({
|
options = (attr.options as any[]).map((opt: any) => ({
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
label: opt.label || String(opt.value),
|
||||||
}));
|
}));
|
||||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||||
} else if (attr.type === 'textarea') fieldType = 'textarea';
|
} else if (attr.type === 'textarea') fieldType = 'textarea';
|
||||||
|
|
|
||||||
|
|
@ -237,24 +237,18 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value, label: opt.label || String(opt.value)
|
||||||
? opt.label
|
}));
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
|
||||||
return { value: opt.value, label: labelValue };
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
} else if (attr.type === 'multiselect') {
|
} else if (attr.type === 'multiselect') {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value, label: opt.label || String(opt.value)
|
||||||
? opt.label
|
}));
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
|
||||||
return { value: opt.value, label: labelValue };
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
@ -303,24 +297,18 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value, label: opt.label || String(opt.value)
|
||||||
? opt.label
|
}));
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
|
||||||
return { value: opt.value, label: labelValue };
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
} else if (attr.type === 'multiselect') {
|
} else if (attr.type === 'multiselect') {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value, label: opt.label || String(opt.value)
|
||||||
? opt.label
|
}));
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
|
||||||
return { value: opt.value, label: labelValue };
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,15 +193,10 @@ export function useTrusteeAccess() {
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,15 +193,10 @@ export function useTrusteeContracts() {
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -194,15 +194,10 @@ export function useTrusteeDocuments() {
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,30 +191,20 @@ export function useTrusteeOrganisations() {
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
} else if (attr.type === 'multiselect') {
|
} else if (attr.type === 'multiselect') {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,15 +181,10 @@ export function useTrusteePositionDocuments() {
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,15 +207,10 @@ export function useTrusteePositions() {
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,15 +201,10 @@ export function useTrusteeRoles() {
|
||||||
} else if (attr.type === 'select') {
|
} else if (attr.type === 'select') {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map((opt: any) => {
|
options = attr.options.map((opt: any) => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export interface UserMandateResponse {
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: string;
|
id: string;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
description?: string | { [key: string]: string };
|
description?: string;
|
||||||
mandateId?: string;
|
mandateId?: string;
|
||||||
featureInstanceId?: string;
|
featureInstanceId?: string;
|
||||||
featureCode?: string;
|
featureCode?: string;
|
||||||
|
|
@ -60,7 +60,7 @@ export interface Role {
|
||||||
|
|
||||||
export interface Mandate {
|
export interface Mandate {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | { [key: string]: string };
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ export function useCurrentUser() {
|
||||||
// Re-export AttributeOption for backward compatibility
|
// Re-export AttributeOption for backward compatibility
|
||||||
export interface AttributeOption {
|
export interface AttributeOption {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string | { [key: string]: string };
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Organization users hook (list, update, delete) - following prompts/workflows pattern
|
// Organization users hook (list, update, delete) - following prompts/workflows pattern
|
||||||
|
|
@ -436,15 +436,10 @@ export function useOrgUsers() {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
|
|
@ -453,15 +448,10 @@ export function useOrgUsers() {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
value: opt.value,
|
||||||
? opt.label
|
label: opt.label || String(opt.value)
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
}));
|
||||||
return {
|
|
||||||
value: opt.value,
|
|
||||||
label: labelValue
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
// Options reference (e.g., "user.role", "auth.authority")
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
|
|
@ -582,12 +572,9 @@ export function useOrgUsers() {
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => {
|
||||||
const labelValue = typeof opt.label === 'string'
|
|
||||||
? opt.label
|
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
|
||||||
return {
|
return {
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: labelValue
|
label: opt.label || String(opt.value)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
|
|
@ -596,15 +583,10 @@ export function useOrgUsers() {
|
||||||
} else if (attrType === 'multiselect') {
|
} else if (attrType === 'multiselect') {
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
|
||||||
? opt.label
|
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
|
||||||
return {
|
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: labelValue
|
label: opt.label || String(opt.value)
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export type { AttributeDefinition } from '../api/attributesApi';
|
||||||
// Attribute option interface (from backend)
|
// Attribute option interface (from backend)
|
||||||
export interface AttributeOption {
|
export interface AttributeOption {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string | { [key: string]: string }; // Can be string or object with language keys
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination parameters
|
// Pagination parameters
|
||||||
|
|
@ -300,15 +300,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
||||||
fieldType = 'enum';
|
fieldType = 'enum';
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
|
||||||
? opt.label
|
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
|
||||||
return {
|
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: labelValue
|
label: opt.label || String(opt.value)
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
@ -316,15 +311,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
||||||
fieldType = 'multiselect';
|
fieldType = 'multiselect';
|
||||||
// Handle options - can be array or string reference
|
// Handle options - can be array or string reference
|
||||||
if (Array.isArray(attr.options)) {
|
if (Array.isArray(attr.options)) {
|
||||||
options = attr.options.map(opt => {
|
options = attr.options.map(opt => ({
|
||||||
const labelValue = typeof opt.label === 'string'
|
|
||||||
? opt.label
|
|
||||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
|
||||||
return {
|
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: labelValue
|
label: opt.label || String(opt.value)
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
} else if (typeof attr.options === 'string') {
|
} else if (typeof attr.options === 'string') {
|
||||||
optionsReference = attr.options;
|
optionsReference = attr.options;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@
|
||||||
* Stellt den Instanz-Kontext bereit und rendert Sidebar + Content.
|
* Stellt den Instanz-Kontext bereit und rendert Sidebar + Content.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Outlet, Navigate, useLocation } from 'react-router-dom';
|
import { Outlet, Navigate, useLocation } from 'react-router-dom';
|
||||||
import { useCurrentInstance } from '../hooks/useCurrentInstance';
|
import { useCurrentInstance } from '../hooks/useCurrentInstance';
|
||||||
import { useFeaturesInitialized, useFeaturesLoading } from '../stores/featureStore';
|
import { useFeaturesInitialized, useFeaturesLoading } from '../stores/featureStore';
|
||||||
|
import useNavigation from '../hooks/useNavigation';
|
||||||
import styles from './FeatureLayout.module.css';
|
import styles from './FeatureLayout.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../providers/language/LanguageContext';
|
import { useLanguage } from '../providers/language/LanguageContext';
|
||||||
|
|
@ -44,7 +45,7 @@ const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' })
|
||||||
<h2>{t('Zugriff nicht möglich')}</h2>
|
<h2>{t('Zugriff nicht möglich')}</h2>
|
||||||
<p>{message}</p>
|
<p>{message}</p>
|
||||||
<a href={returnPath} className={styles.errorLink}>
|
<a href={returnPath} className={styles.errorLink}>
|
||||||
Zurück zur Übersicht
|
{t('Zurück zur Übersicht')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -65,10 +66,27 @@ const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' })
|
||||||
* Bei Erfolg: Rendert <Outlet /> für die verschachtelten Routes
|
* Bei Erfolg: Rendert <Outlet /> für die verschachtelten Routes
|
||||||
*/
|
*/
|
||||||
export const FeatureLayout: React.FC = () => {
|
export const FeatureLayout: React.FC = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const initialized = useFeaturesInitialized();
|
const initialized = useFeaturesInitialized();
|
||||||
const loading = useFeaturesLoading();
|
const loading = useFeaturesLoading();
|
||||||
const { instance, mandate, feature, isValid, isLoading } = useCurrentInstance();
|
const { instance, mandate, feature, isValid, isLoading, mandateId, featureCode, instanceId } = useCurrentInstance();
|
||||||
|
const { dynamicBlock } = useNavigation();
|
||||||
|
|
||||||
|
const navLabels = useMemo(() => {
|
||||||
|
if (!dynamicBlock || !mandateId) return null;
|
||||||
|
const navMandate = dynamicBlock.mandates.find(m => m.id === mandateId);
|
||||||
|
if (!navMandate) return null;
|
||||||
|
const navFeature = featureCode ? navMandate.features.find(f => f.uiComponent === featureCode) : undefined;
|
||||||
|
const navInstance = navFeature && instanceId
|
||||||
|
? navFeature.instances.find(i => i.id === instanceId)
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
mandate: t(navMandate.uiLabel),
|
||||||
|
feature: navFeature ? t(navFeature.uiLabel) : undefined,
|
||||||
|
instance: navInstance ? t(navInstance.uiLabel) : undefined,
|
||||||
|
};
|
||||||
|
}, [dynamicBlock, mandateId, featureCode, instanceId, t]);
|
||||||
|
|
||||||
// Warten bis Features geladen sind
|
// Warten bis Features geladen sind
|
||||||
if (!initialized || loading || isLoading) {
|
if (!initialized || loading || isLoading) {
|
||||||
|
|
@ -86,7 +104,7 @@ export const FeatureLayout: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorScreen
|
<ErrorScreen
|
||||||
message="Die angeforderte Feature-Instanz existiert nicht oder Sie haben keinen Zugriff."
|
message={t('Die angeforderte Feature-Instanz existiert nicht oder Sie haben keinen Zugriff.')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -97,11 +115,11 @@ export const FeatureLayout: React.FC = () => {
|
||||||
{/* Header mit Instanz-Info */}
|
{/* Header mit Instanz-Info */}
|
||||||
<header className={styles.featureHeader}>
|
<header className={styles.featureHeader}>
|
||||||
<div className={styles.breadcrumb}>
|
<div className={styles.breadcrumb}>
|
||||||
<span className={styles.mandateName}>{mandate?.name}</span>
|
<span className={styles.mandateName}>{navLabels?.mandate || mandate?.label || mandate?.name}</span>
|
||||||
<span className={styles.separator}>/</span>
|
<span className={styles.separator}>/</span>
|
||||||
<span className={styles.featureName}>{feature?.label?.de || feature?.code}</span>
|
<span className={styles.featureName}>{navLabels?.feature || feature?.code}</span>
|
||||||
<span className={styles.separator}>/</span>
|
<span className={styles.separator}>/</span>
|
||||||
<span className={styles.instanceName}>{instance?.instanceLabel}</span>
|
<span className={styles.instanceName}>{navLabels?.instance || instance?.instanceLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.roleIndicator}>
|
<div className={styles.roleIndicator}>
|
||||||
<span className={styles.roleBadge}>{instance?.userRoles?.join(', ') || '-'}</span>
|
<span className={styles.roleBadge}>{instance?.userRoles?.join(', ') || '-'}</span>
|
||||||
|
|
@ -133,6 +151,7 @@ export const ProtectedFeatureRoute: React.FC<ProtectedFeatureRouteProps> = ({
|
||||||
requiredView,
|
requiredView,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const { instance, isValid } = useCurrentInstance();
|
const { instance, isValid } = useCurrentInstance();
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
|
@ -146,7 +165,7 @@ export const ProtectedFeatureRoute: React.FC<ProtectedFeatureRouteProps> = ({
|
||||||
if (!hasViewAccess) {
|
if (!hasViewAccess) {
|
||||||
return (
|
return (
|
||||||
<ErrorScreen
|
<ErrorScreen
|
||||||
message={`Sie haben keine Berechtigung für diesen Bereich (${requiredView}).`}
|
message={t('Sie haben keine Berechtigung für diesen Bereich ({view}).', { view: requiredView })}
|
||||||
returnPath={`/mandates/${instance?.mandateId}/${instance?.featureCode}/${instance?.id}`}
|
returnPath={`/mandates/${instance?.mandateId}/${instance?.featureCode}/${instance?.id}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -84,13 +84,13 @@ const MainLayoutInner: React.FC = () => {
|
||||||
<nav className={styles.navigation}>
|
<nav className={styles.navigation}>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className={styles.loadingNav}>
|
<div className={styles.loadingNav}>
|
||||||
Lade Navigation...
|
{t('Lade Navigation…')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className={styles.errorNav}>
|
<div className={styles.errorNav}>
|
||||||
Fehler: {error}
|
{t('Fehler')}: {error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,8 @@ export const AutomationsDashboardPage: React.FC = () => {
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
formatter: (v: string) => (
|
formatter: (v: string) => (
|
||||||
<span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600, textTransform: 'capitalize' }}>
|
<span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600 }}>
|
||||||
{v}
|
{t(v === 'completed' ? 'Abgeschlossen' : v === 'failed' ? 'Fehlgeschlagen' : v === 'running' ? 'Laufend' : v)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,12 @@ export const DashboardPage: React.FC = () => {
|
||||||
<h1>{t('Übersicht')}</h1>
|
<h1>{t('Übersicht')}</h1>
|
||||||
{totalInstances > 0 && (
|
{totalInstances > 0 && (
|
||||||
<p className={styles.subtitle}>
|
<p className={styles.subtitle}>
|
||||||
Du hast Zugriff auf {totalInstances} Feature-Instanz{totalInstances !== 1 ? 'en' : ''} in {totalMandates} Mandant{totalMandates !== 1 ? 'en' : ''}.
|
{t('Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}.', {
|
||||||
|
instanceCount: totalInstances,
|
||||||
|
instanceWord: totalInstances === 1 ? t('Feature-Instanz') : t('Feature-Instanzen'),
|
||||||
|
mandateCount: totalMandates,
|
||||||
|
mandateWord: totalMandates === 1 ? t('Mandant') : t('Mandanten'),
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,12 @@ const ChatworkflowDashboard: React.FC = () => {
|
||||||
|
|
||||||
const ChatworkflowRuns: React.FC = () => {
|
const ChatworkflowRuns: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
return <PlaceholderView title="Runs" description={t('Workflow-Ausführungen')} />;
|
return <PlaceholderView title={t('Ausführungen')} description={t('Workflow-Ausführungen')} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatworkflowFiles: React.FC = () => {
|
const ChatworkflowFiles: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
return <PlaceholderView title={t('Dateien')} description="Workflow-Dateien" />;
|
return <PlaceholderView title={t('Dateien')} description={t('Workflow-Dateien')} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chatbot Views
|
// Chatbot Views
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const GDPRPage: React.FC = () => {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to load GDPR consent info:', error);
|
console.error('Failed to load GDPR consent info:', error);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
setConsentError('Consent information could not be loaded.');
|
setConsentError(t('Einwilligungsinformationen konnten nicht geladen werden.'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
|
@ -82,7 +82,7 @@ export const GDPRPage: React.FC = () => {
|
||||||
return () => {
|
return () => {
|
||||||
isActive = false;
|
isActive = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
const handleDataExport = async () => {
|
const handleDataExport = async () => {
|
||||||
if (isActionLocked) return;
|
if (isActionLocked) return;
|
||||||
|
|
@ -91,10 +91,10 @@ export const GDPRPage: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/api/user/me/data-export');
|
const response = await api.get('/api/user/me/data-export');
|
||||||
downloadJson(response.data, 'gdpr-data-export.json');
|
downloadJson(response.data, 'gdpr-data-export.json');
|
||||||
setActionMessage({ type: 'success', text: 'Data export downloaded.' });
|
setActionMessage({ type: 'success', text: t('Datenexport heruntergeladen.') });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('GDPR export failed:', error);
|
console.error('GDPR export failed:', error);
|
||||||
setActionMessage({ type: 'error', text: 'Data export failed. Please try again.' });
|
setActionMessage({ type: 'error', text: t('Datenexport fehlgeschlagen. Bitte erneut versuchen.') });
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -109,10 +109,10 @@ export const GDPRPage: React.FC = () => {
|
||||||
headers: { Accept: 'application/ld+json' }
|
headers: { Accept: 'application/ld+json' }
|
||||||
});
|
});
|
||||||
downloadJson(response.data, 'gdpr-data-portability.json', 'application/ld+json');
|
downloadJson(response.data, 'gdpr-data-portability.json', 'application/ld+json');
|
||||||
setActionMessage({ type: 'success', text: 'Portable export downloaded.' });
|
setActionMessage({ type: 'success', text: t('Portabler Export heruntergeladen.') });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('GDPR portability export failed:', error);
|
console.error('GDPR portability export failed:', error);
|
||||||
setActionMessage({ type: 'error', text: 'Portable export failed. Please try again.' });
|
setActionMessage({ type: 'error', text: t('Portabler Export fehlgeschlagen. Bitte erneut versuchen.') });
|
||||||
} finally {
|
} finally {
|
||||||
setIsPortabilityExporting(false);
|
setIsPortabilityExporting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +121,7 @@ export const GDPRPage: React.FC = () => {
|
||||||
const handleDeleteAccount = async () => {
|
const handleDeleteAccount = async () => {
|
||||||
setActionMessage(null);
|
setActionMessage(null);
|
||||||
if (deleteConfirmText !== 'LOESCHEN') {
|
if (deleteConfirmText !== 'LOESCHEN') {
|
||||||
setActionMessage({ type: 'error', text: 'Please type LOESCHEN to confirm deletion.' });
|
setActionMessage({ type: 'error', text: t('Bitte geben Sie LOESCHEN ein, um die Löschung zu bestätigen.') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,11 +132,11 @@ export const GDPRPage: React.FC = () => {
|
||||||
sessionStorage.removeItem('auth_authority');
|
sessionStorage.removeItem('auth_authority');
|
||||||
clearUserDataCache();
|
clearUserDataCache();
|
||||||
setIsDeleted(true);
|
setIsDeleted(true);
|
||||||
setActionMessage({ type: 'success', text: 'Account deleted. Redirecting to login...' });
|
setActionMessage({ type: 'success', text: t('Konto gelöscht. Weiterleitung zur Anmeldung…') });
|
||||||
window.location.replace('/login');
|
window.location.replace('/login');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('GDPR deletion failed:', error);
|
console.error('GDPR deletion failed:', error);
|
||||||
setActionMessage({ type: 'error', text: 'Account deletion failed. Please try again.' });
|
setActionMessage({ type: 'error', text: t('Kontolöschung fehlgeschlagen. Bitte erneut versuchen.') });
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -148,14 +148,14 @@ export const GDPRPage: React.FC = () => {
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.title}>
|
<h1 className={styles.title}>
|
||||||
<FaShieldAlt className={styles.titleIcon} />
|
<FaShieldAlt className={styles.titleIcon} />
|
||||||
GDPR / Privacy
|
{t('DSGVO / Datenschutz')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className={styles.subtitle}>
|
<p className={styles.subtitle}>
|
||||||
Manage your personal data exports and account deletion.
|
{t('Verwalten Sie Ihre personenbezogenen Datenexporte und Kontolöschung.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/settings" className={styles.backLink}>
|
<Link to="/settings" className={styles.backLink}>
|
||||||
Back to Settings
|
{t('Zurück zu Einstellungen')}
|
||||||
</Link>
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -174,12 +174,12 @@ export const GDPRPage: React.FC = () => {
|
||||||
{isExporting ? (
|
{isExporting ? (
|
||||||
<span className={styles.buttonSpinner}>
|
<span className={styles.buttonSpinner}>
|
||||||
<FaSpinner />
|
<FaSpinner />
|
||||||
Exporting...
|
{t('Export wird erstellt…')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaDownload />
|
<FaDownload />
|
||||||
Export data
|
{t('Daten exportieren')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -196,12 +196,12 @@ export const GDPRPage: React.FC = () => {
|
||||||
{isPortabilityExporting ? (
|
{isPortabilityExporting ? (
|
||||||
<span className={styles.buttonSpinner}>
|
<span className={styles.buttonSpinner}>
|
||||||
<FaSpinner />
|
<FaSpinner />
|
||||||
Exporting...
|
{t('Export wird erstellt…')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaFileExport />
|
<FaFileExport />
|
||||||
Export portable data
|
{t('Portabler Datenexport')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -217,13 +217,15 @@ export const GDPRPage: React.FC = () => {
|
||||||
disabled={isActionLocked}
|
disabled={isActionLocked}
|
||||||
>
|
>
|
||||||
<FaTrash />
|
<FaTrash />
|
||||||
Start deletion
|
{t('Löschung starten')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{showDeleteConfirm && (
|
{showDeleteConfirm && (
|
||||||
<div className={styles.deleteConfirm}>
|
<div className={styles.deleteConfirm}>
|
||||||
<p className={styles.deleteWarning}>
|
<p className={styles.deleteWarning}>
|
||||||
This action is irreversible. Type <strong>LOESCHEN</strong> to confirm.
|
{t('Diese Aktion ist unwiderruflich. Geben Sie {word} ein, um zu bestätigen.', {
|
||||||
|
word: 'LOESCHEN',
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
className={styles.deleteInput}
|
className={styles.deleteInput}
|
||||||
|
|
@ -241,7 +243,7 @@ export const GDPRPage: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
>
|
>
|
||||||
Cancel
|
{t('Abbrechen')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.dangerButton}
|
className={styles.dangerButton}
|
||||||
|
|
@ -251,12 +253,12 @@ export const GDPRPage: React.FC = () => {
|
||||||
{isDeleting ? (
|
{isDeleting ? (
|
||||||
<span className={styles.buttonSpinner}>
|
<span className={styles.buttonSpinner}>
|
||||||
<FaSpinner />
|
<FaSpinner />
|
||||||
Deleting...
|
{t('Wird gelöscht…')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaTrash />
|
<FaTrash />
|
||||||
Confirm deletion
|
{t('Löschung bestätigen')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -314,7 +316,7 @@ export const GDPRPage: React.FC = () => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.infoBlock}>
|
<div className={styles.infoBlock}>
|
||||||
<h3>Contact</h3>
|
<h3>{t('Kontakt')}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries({
|
{Object.entries({
|
||||||
...(consentInfo.contact || {}),
|
...(consentInfo.contact || {}),
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export const InvitePage: React.FC = () => {
|
||||||
sessionStorage.removeItem('auth_authority');
|
sessionStorage.removeItem('auth_authority');
|
||||||
handleLoginRedirect();
|
handleLoginRedirect();
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Fehler beim Annehmen der Einladung');
|
setError(result.error || t('Fehler beim Annehmen der Einladung'));
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccepting(false);
|
setAccepting(false);
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ function Login() {
|
||||||
}}
|
}}
|
||||||
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
|
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
|
||||||
/>
|
/>
|
||||||
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>{t('Benutzername')}</label>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.floatingLabelInput}>
|
<div className={styles.floatingLabelInput}>
|
||||||
<input
|
<input
|
||||||
|
|
@ -190,7 +190,7 @@ function Login() {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.disclaimer}>
|
<div className={styles.disclaimer}>
|
||||||
<p>
|
<p>
|
||||||
Mit der Anmeldung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.
|
{t('Mit der Anmeldung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -198,7 +198,7 @@ function Login() {
|
||||||
onClick={handleCredentialLogin}
|
onClick={handleCredentialLogin}
|
||||||
disabled={isLoginLoading}
|
disabled={isLoginLoading}
|
||||||
>
|
>
|
||||||
{isLoginLoading ? "wird geladen..." : "Anmelden"}
|
{isLoginLoading ? t('wird geladen…') : t('Anmelden')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={styles.passwordResetLink}>
|
<div className={styles.passwordResetLink}>
|
||||||
|
|
@ -206,12 +206,12 @@ function Login() {
|
||||||
className={styles.textButton}
|
className={styles.textButton}
|
||||||
onClick={() => navigate("/password-reset-request")}
|
onClick={() => navigate("/password-reset-request")}
|
||||||
>
|
>
|
||||||
Passwort vergessen?
|
{t('Passwort vergessen?')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.divider}>
|
<div className={styles.divider}>
|
||||||
<span>oder</span>
|
<span>{t('oder')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -221,7 +221,7 @@ function Login() {
|
||||||
>
|
>
|
||||||
<div className={styles.buttonContent}>
|
<div className={styles.buttonContent}>
|
||||||
<FaMicrosoft />
|
<FaMicrosoft />
|
||||||
{isMsalLoading ? "Signing in..." : "Mit Microsoft anmelden"}
|
{isMsalLoading ? t('Anmeldung läuft…') : t('Mit Microsoft anmelden')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@ function Login() {
|
||||||
>
|
>
|
||||||
<div className={styles.buttonContent}>
|
<div className={styles.buttonContent}>
|
||||||
<FaGoogle />
|
<FaGoogle />
|
||||||
{isGoogleLoading ? "Signing in..." : "Mit Google anmelden"}
|
{isGoogleLoading ? t('Anmeldung läuft…') : t('Mit Google anmelden')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
@ -245,7 +245,7 @@ function Login() {
|
||||||
className={styles.ctaPrimary}
|
className={styles.ctaPrimary}
|
||||||
onClick={() => navigate('/register', { state: location.state })}
|
onClick={() => navigate('/register', { state: location.state })}
|
||||||
>
|
>
|
||||||
Kostenlos registrieren
|
{t('Kostenlos registrieren')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,26 +28,26 @@ function PasswordResetRequest() {
|
||||||
setValidationError(null);
|
setValidationError(null);
|
||||||
|
|
||||||
if (!username.trim()) {
|
if (!username.trim()) {
|
||||||
setValidationError('Bitte geben Sie Ihren Benutzernamen ein.');
|
setValidationError(t('Bitte geben Sie Ihren Benutzernamen ein.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await requestReset(username.trim());
|
await requestReset(username.trim());
|
||||||
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
|
setSuccessMessage(t('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.'));
|
||||||
|
|
||||||
// Redirect to login after delay
|
// Redirect to login after delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/login', {
|
navigate('/login', {
|
||||||
state: {
|
state: {
|
||||||
passwordResetRequested: true,
|
passwordResetRequested: true,
|
||||||
message: 'Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.'
|
message: t('Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// For security, still show success message even on error
|
// For security, still show success message even on error
|
||||||
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
|
setSuccessMessage(t('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.'));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
@ -97,7 +97,7 @@ function PasswordResetRequest() {
|
||||||
}}
|
}}
|
||||||
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
|
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
|
||||||
/>
|
/>
|
||||||
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>{t('Benutzername')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.infoMessage}>
|
<div className={styles.infoMessage}>
|
||||||
|
|
@ -109,7 +109,7 @@ function PasswordResetRequest() {
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? "Wird gesendet..." : "Reset-Link anfordern"}
|
{isLoading ? t('Wird gesendet…') : t('Reset-Link anfordern')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -120,7 +120,7 @@ function PasswordResetRequest() {
|
||||||
className={styles.textButton}
|
className={styles.textButton}
|
||||||
onClick={() => navigate("/login")}
|
onClick={() => navigate("/login")}
|
||||||
>
|
>
|
||||||
Login
|
{t('Login')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,12 @@ function Register() {
|
||||||
|
|
||||||
const _validateForm = (): boolean => {
|
const _validateForm = (): boolean => {
|
||||||
if (!formData.username || !formData.email || !formData.fullName) {
|
if (!formData.username || !formData.email || !formData.fullName) {
|
||||||
setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
|
setValidationError(t('Bitte füllen Sie alle Pflichtfelder aus.'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.email.includes('@')) {
|
if (!formData.email.includes('@')) {
|
||||||
setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
|
setValidationError(t('Bitte geben Sie eine gültige E-Mail-Adresse ein.'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,19 +83,19 @@ function Register() {
|
||||||
if (!availabilityResult.available) {
|
if (!availabilityResult.available) {
|
||||||
const errorMessage = availabilityResult.message || 'Username is not available';
|
const errorMessage = availabilityResult.message || 'Username is not available';
|
||||||
if (errorMessage === 'Username is already taken') {
|
if (errorMessage === 'Username is already taken') {
|
||||||
setValidationError('Benutzername ist bereits vergeben');
|
setValidationError(t('Benutzername ist bereits vergeben'));
|
||||||
setUsernameHighlight(true);
|
setUsernameHighlight(true);
|
||||||
} else {
|
} else {
|
||||||
setValidationError('Benutzername ist nicht verfügbar');
|
setValidationError(t('Benutzername ist nicht verfügbar'));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await register({ ...formData, registrationType: 'personal' });
|
await register({ ...formData, registrationType: 'personal' });
|
||||||
|
|
||||||
let message = 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.';
|
let message = t('Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.');
|
||||||
if (hasPendingInvitation) {
|
if (hasPendingInvitation) {
|
||||||
message += ' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.';
|
message += t(' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
setSuccessMessage(message);
|
setSuccessMessage(message);
|
||||||
|
|
@ -104,7 +104,7 @@ function Register() {
|
||||||
navigate('/login', {
|
navigate('/login', {
|
||||||
state: {
|
state: {
|
||||||
registered: true,
|
registered: true,
|
||||||
message: 'Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.',
|
message: t('Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.'),
|
||||||
...(location.state || {})
|
...(location.state || {})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -116,9 +116,9 @@ function Register() {
|
||||||
|
|
||||||
const _getErrorMessage = () => {
|
const _getErrorMessage = () => {
|
||||||
if (validationError) return validationError;
|
if (validationError) return validationError;
|
||||||
if (registerError) return typeof registerError === 'string' ? registerError : 'Registration failed';
|
if (registerError) return typeof registerError === 'string' ? registerError : t('Registrierung fehlgeschlagen');
|
||||||
if (msalError) return typeof msalError === 'string' ? msalError : 'Microsoft registration failed';
|
if (msalError) return typeof msalError === 'string' ? msalError : t('Microsoft-Registrierung fehlgeschlagen');
|
||||||
if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : 'Username availability check failed';
|
if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : t('Benutzernamen-Prüfung fehlgeschlagen');
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -163,7 +163,7 @@ function Register() {
|
||||||
onBlur={() => setUsernameFocused(false)}
|
onBlur={() => setUsernameFocused(false)}
|
||||||
className={`${styles.input} ${usernameFocused || formData.username ? styles.focused : ''} ${usernameHighlight ? styles.usernameError : ''}`}
|
className={`${styles.input} ${usernameFocused || formData.username ? styles.focused : ''} ${usernameHighlight ? styles.usernameError : ''}`}
|
||||||
/>
|
/>
|
||||||
<label className={usernameFocused || formData.username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
<label className={usernameFocused || formData.username ? styles.focusedLabel : styles.label}>{t('Benutzername')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.floatingLabelInput}>
|
<div className={styles.floatingLabelInput}>
|
||||||
|
|
@ -177,7 +177,7 @@ function Register() {
|
||||||
onBlur={() => setEmailFocused(false)}
|
onBlur={() => setEmailFocused(false)}
|
||||||
className={`${styles.input} ${emailFocused || formData.email ? styles.focused : ''}`}
|
className={`${styles.input} ${emailFocused || formData.email ? styles.focused : ''}`}
|
||||||
/>
|
/>
|
||||||
<label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>E-Mail</label>
|
<label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>{t('E-Mail')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.floatingLabelInput}>
|
<div className={styles.floatingLabelInput}>
|
||||||
|
|
@ -200,7 +200,7 @@ function Register() {
|
||||||
|
|
||||||
<div className={styles.disclaimer}>
|
<div className={styles.disclaimer}>
|
||||||
<p>
|
<p>
|
||||||
Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.
|
{t('Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -209,7 +209,7 @@ function Register() {
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isLoading || isChecking}
|
disabled={isLoading || isChecking}
|
||||||
>
|
>
|
||||||
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : 'Kostenlos registrieren'}
|
{isLoading ? t('Registrierung läuft…') : isChecking ? t('Benutzername wird geprüft…') : t('Kostenlos registrieren')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -220,7 +220,7 @@ function Register() {
|
||||||
className={styles.textButton}
|
className={styles.textButton}
|
||||||
onClick={() => navigate("/login", { state: location.state })}
|
onClick={() => navigate("/login", { state: location.state })}
|
||||||
>
|
>
|
||||||
Jetzt anmelden
|
{t('Jetzt anmelden')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,11 @@ function Reset() {
|
||||||
|
|
||||||
// Validate token exists and format
|
// Validate token exists and format
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
|
setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'));
|
||||||
} else if (!_isValidUUID(token)) {
|
} else if (!_isValidUUID(token)) {
|
||||||
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
|
setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'));
|
||||||
}
|
}
|
||||||
}, [token]);
|
}, [token, t]);
|
||||||
|
|
||||||
const _isValidUUID = (str: string): boolean => {
|
const _isValidUUID = (str: string): boolean => {
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
|
@ -44,12 +44,12 @@ function Reset() {
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
if (!password || password.length < 8) {
|
if (!password || password.length < 8) {
|
||||||
setValidationError('Passwort muss mindestens 8 Zeichen lang sein.');
|
setValidationError(t('Passwort muss mindestens 8 Zeichen lang sein.'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
setValidationError('Die Passwörter stimmen nicht überein.');
|
setValidationError(t('Die Passwörter stimmen nicht überein.'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,28 +65,28 @@ function Reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setValidationError('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.');
|
setValidationError(t('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resetPassword(token, password);
|
await resetPassword(token, password);
|
||||||
setSuccessMessage('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet...');
|
setSuccessMessage(t('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet…'));
|
||||||
|
|
||||||
// Redirect to login after delay
|
// Redirect to login after delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/login', {
|
navigate('/login', {
|
||||||
state: {
|
state: {
|
||||||
passwordReset: true,
|
passwordReset: true,
|
||||||
message: 'Passwort erfolgreich geändert. Bitte melden Sie sich an.'
|
message: t('Passwort erfolgreich geändert. Bitte melden Sie sich an.')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Error is already set by the hook
|
// Error is already set by the hook
|
||||||
const errorMessage = err?.response?.data?.detail || err?.message || 'Passwort-Zurücksetzung fehlgeschlagen.';
|
const errorMessage = err?.response?.data?.detail || err?.message || t('Passwort-Zurücksetzung fehlgeschlagen.');
|
||||||
if (errorMessage.includes('abgelaufen') || errorMessage.includes('expired') || errorMessage.includes('Ungültig') || errorMessage.includes('invalid')) {
|
if (errorMessage.includes('abgelaufen') || errorMessage.includes('expired') || errorMessage.includes('Ungültig') || errorMessage.includes('invalid')) {
|
||||||
setValidationError('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.');
|
setValidationError(t('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.'));
|
||||||
} else {
|
} else {
|
||||||
setValidationError(errorMessage);
|
setValidationError(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +115,7 @@ function Reset() {
|
||||||
className={styles.textButton}
|
className={styles.textButton}
|
||||||
onClick={() => navigate("/password-reset-request")}
|
onClick={() => navigate("/password-reset-request")}
|
||||||
>
|
>
|
||||||
Neuen Reset-Link anfordern
|
{t('Neuen Reset-Link anfordern')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.registerLink}>
|
<div className={styles.registerLink}>
|
||||||
|
|
@ -124,7 +124,7 @@ function Reset() {
|
||||||
className={styles.textButton}
|
className={styles.textButton}
|
||||||
onClick={() => navigate("/login")}
|
onClick={() => navigate("/login")}
|
||||||
>
|
>
|
||||||
Login
|
{t('Login')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -200,7 +200,7 @@ function Reset() {
|
||||||
className={`${styles.button} ${styles.loginButton}`}
|
className={`${styles.button} ${styles.loginButton}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? "Wird gespeichert..." : "Passwort setzen"}
|
{isLoading ? t('Wird gespeichert…') : t('Passwort setzen')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
@ -211,7 +211,7 @@ function Reset() {
|
||||||
className={styles.textButton}
|
className={styles.textButton}
|
||||||
onClick={() => navigate("/login")}
|
onClick={() => navigate("/login")}
|
||||||
>
|
>
|
||||||
Login
|
{t('Login')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, us
|
||||||
await onSave(formData);
|
await onSave(formData);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Fehler beim Speichern des Profils');
|
setError(err.message || t('Fehler beim Speichern des Profils'));
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
|
|
@ -520,17 +520,17 @@ export const SettingsPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<div className={styles.userInfoCard}>
|
<div className={styles.userInfoCard}>
|
||||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>Benutzername</span><span className={styles.userInfoValue}>{currentUser.username}</span></div>
|
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>{t('Benutzername')}</span><span className={styles.userInfoValue}>{currentUser.username}</span></div>
|
||||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>Name</span><span className={styles.userInfoValue}>{currentUser.fullName || '-'}</span></div>
|
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>{t('Name')}</span><span className={styles.userInfoValue}>{currentUser.fullName || '-'}</span></div>
|
||||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>E-Mail</span><span className={styles.userInfoValue}>{currentUser.email || '-'}</span></div>
|
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>{t('E-Mail')}</span><span className={styles.userInfoValue}>{currentUser.email || '-'}</span></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h2 className={styles.sectionTitle}>Ueber</h2>
|
<h2 className={styles.sectionTitle}>{t('Applikation')}</h2>
|
||||||
<div className={styles.infoCard}>
|
<div className={styles.infoCard}>
|
||||||
<div className={styles.infoRow}><span className={styles.infoLabel}>Version</span><span className={styles.infoValue}>2.0.0</span></div>
|
<div className={styles.infoRow}><span className={styles.infoLabel}>{t('Version')}</span><span className={styles.infoValue}>2.0.0</span></div>
|
||||||
<div className={styles.infoRow}><span className={styles.infoLabel}>Build</span><span className={styles.infoValue}>2026.03.23</span></div>
|
<div className={styles.infoRow}><span className={styles.infoLabel}>{t('Build')}</span><span className={styles.infoValue}>2026.03.23</span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,13 @@ import { FeatureInstanceWizard } from './wizards/FeatureInstanceWizard';
|
||||||
import { InstanceHierarchyView } from './InstanceHierarchyView';
|
import { InstanceHierarchyView } from './InstanceHierarchyView';
|
||||||
|
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
import { labelAsI18nKey } from '../../types/mandate';
|
|
||||||
|
|
||||||
function getMandateName(mandate: Mandate): string {
|
function getMandateName(mandate: Mandate): string {
|
||||||
if (mandate.label) return mandate.label;
|
return mandate.label || mandate.name || mandate.id;
|
||||||
if (typeof mandate.name === 'object') {
|
|
||||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
|
||||||
}
|
|
||||||
return mandate.name || mandate.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeatureLabel(feature: Feature, t: (k: string) => string): string {
|
function getFeatureLabel(feature: Feature, t: (k: string) => string): string {
|
||||||
return t(labelAsI18nKey(feature.label, feature.code));
|
return t(feature.label || feature.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstanceWithStats extends FeatureInstance {
|
export interface InstanceWithStats extends FeatureInstance {
|
||||||
|
|
@ -146,15 +141,19 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
showSuccess(
|
showSuccess(
|
||||||
'Rollen synchronisiert',
|
t('Rollen synchronisiert'),
|
||||||
`Hinzugefügt: ${result.data.added}, Entfernt: ${result.data.removed}, Unverändert: ${result.data.unchanged}`
|
t('Hinzugefügt: {added}, Entfernt: {removed}, Unverändert: {unchanged}', {
|
||||||
|
added: result.data.added,
|
||||||
|
removed: result.data.removed,
|
||||||
|
unchanged: result.data.unchanged,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
fetchInstances(selectedMandateId, selectedFeatureCode || undefined);
|
fetchInstances(selectedMandateId, selectedFeatureCode || undefined);
|
||||||
} else {
|
} else {
|
||||||
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren');
|
showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren'));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
showError('Fehler', 'Rollen konnten nicht synchronisiert werden');
|
showError(t('Fehler'), t('Rollen konnten nicht synchronisiert werden'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -295,7 +294,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
return {
|
return {
|
||||||
id: inst.id,
|
id: inst.id,
|
||||||
label: inst.label,
|
label: inst.label,
|
||||||
featureLabel: feature ? getFeatureLabel(feature) : inst.featureCode,
|
featureLabel: feature ? getFeatureLabel(feature, t) : inst.featureCode,
|
||||||
userCount: inst.userCount ?? 0,
|
userCount: inst.userCount ?? 0,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
@ -307,9 +306,11 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<div className={styles.adminPage}>
|
<div className={styles.adminPage}>
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<span className={styles.errorIcon}>⚠️</span>
|
<span className={styles.errorIcon}>⚠️</span>
|
||||||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
<p className={styles.errorMessage}>
|
||||||
|
{t('Fehler')}: {error}
|
||||||
|
</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||||||
<FaSync /> Erneut versuchen
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -320,9 +321,9 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<div className={styles.adminPage}>
|
<div className={styles.adminPage}>
|
||||||
<div className={styles.pageHeader}>
|
<div className={styles.pageHeader}>
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.pageTitle}>Zugriffsverwaltung</h1>
|
<h1 className={styles.pageTitle}>{t('Zugriffsverwaltung')}</h1>
|
||||||
<p className={styles.pageSubtitle}>
|
<p className={styles.pageSubtitle}>
|
||||||
Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten
|
{t('Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -334,7 +335,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<div className={styles.filterGroup}>
|
<div className={styles.filterGroup}>
|
||||||
<label className={styles.filterLabel}>
|
<label className={styles.filterLabel}>
|
||||||
<FaBuilding style={{ marginRight: 8 }} />
|
<FaBuilding style={{ marginRight: 8 }} />
|
||||||
Mandant:
|
{t('Mandant')}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className={styles.filterSelect}
|
className={styles.filterSelect}
|
||||||
|
|
@ -352,7 +353,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<div className={styles.filterGroup}>
|
<div className={styles.filterGroup}>
|
||||||
<label className={styles.filterLabel}>
|
<label className={styles.filterLabel}>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
Feature:
|
{t('Feature')}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className={styles.filterSelect}
|
className={styles.filterSelect}
|
||||||
|
|
@ -376,14 +377,14 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
}
|
}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.primaryButton}
|
className={styles.primaryButton}
|
||||||
onClick={() => setShowWizard(true)}
|
onClick={() => setShowWizard(true)}
|
||||||
disabled={features.length === 0}
|
disabled={features.length === 0}
|
||||||
>
|
>
|
||||||
+ Neue Instanz erstellen
|
+ {t('Neue Instanz erstellen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -398,21 +399,21 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
className={viewMode === 'list' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
|
className={viewMode === 'list' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
|
||||||
onClick={() => setViewMode('list')}
|
onClick={() => setViewMode('list')}
|
||||||
>
|
>
|
||||||
<FaList /> Listenansicht
|
<FaList /> {t('Listenansicht')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={viewMode === 'hierarchy' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
|
className={viewMode === 'hierarchy' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
|
||||||
onClick={() => setViewMode('hierarchy')}
|
onClick={() => setViewMode('hierarchy')}
|
||||||
>
|
>
|
||||||
<FaSitemap /> Hierarchie
|
<FaSitemap /> {t('Hierarchie')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/admin/mandates" className={hubStyles.mandatesLink}>
|
<Link to="/admin/mandates" className={hubStyles.mandatesLink}>
|
||||||
<FaBuilding /> Mandanten verwalten
|
<FaBuilding /> {t('Mandanten verwalten')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/admin/user-mandates" className={hubStyles.mandatesLink}>
|
<Link to="/admin/user-mandates" className={hubStyles.mandatesLink}>
|
||||||
<FaUsers /> Mandant-Benutzer
|
<FaUsers /> {t('Mandant-Benutzer')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -423,7 +424,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
instancesByMandate={instancesByMandate}
|
instancesByMandate={instancesByMandate}
|
||||||
instanceUsersMap={instanceUsersMap}
|
instanceUsersMap={instanceUsersMap}
|
||||||
features={features}
|
features={features}
|
||||||
getFeatureLabel={getFeatureLabel}
|
getFeatureLabel={(f) => getFeatureLabel(f, t)}
|
||||||
loading={hierarchyUsersLoading}
|
loading={hierarchyUsersLoading}
|
||||||
onOpenDetail={handleOpenDetail}
|
onOpenDetail={handleOpenDetail}
|
||||||
/>
|
/>
|
||||||
|
|
@ -432,7 +433,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<FaBuilding className={styles.emptyIcon} />
|
<FaBuilding className={styles.emptyIcon} />
|
||||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||||
<p className={styles.emptyDescription}>
|
<p className={styles.emptyDescription}>
|
||||||
Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.
|
{t('Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -444,7 +445,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<span className={hubStyles.statsValue}>
|
<span className={hubStyles.statsValue}>
|
||||||
{loading || statsLoading ? '…' : overviewStats.instances}
|
{loading || statsLoading ? '…' : overviewStats.instances}
|
||||||
</span>
|
</span>
|
||||||
<span className={hubStyles.statsLabel}>Instanzen</span>
|
<span className={hubStyles.statsLabel}>{t('Instanzen')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={hubStyles.statsCard}>
|
<div className={hubStyles.statsCard}>
|
||||||
|
|
@ -469,7 +470,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<div className={hubStyles.diagramCard}>
|
<div className={hubStyles.diagramCard}>
|
||||||
<FaLink className={hubStyles.statsIcon} />
|
<FaLink className={hubStyles.statsIcon} />
|
||||||
<div className={hubStyles.diagramContent}>
|
<div className={hubStyles.diagramContent}>
|
||||||
<span className={hubStyles.diagramTitle}>Beziehungen</span>
|
<span className={hubStyles.diagramTitle}>{t('Beziehungen')}</span>
|
||||||
<div className={hubStyles.diagramFlow}>
|
<div className={hubStyles.diagramFlow}>
|
||||||
<div className={hubStyles.diagramNode}>{relationshipData.mandateName}</div>
|
<div className={hubStyles.diagramNode}>{relationshipData.mandateName}</div>
|
||||||
<div className={hubStyles.diagramNodes}>
|
<div className={hubStyles.diagramNodes}>
|
||||||
|
|
@ -480,7 +481,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
{relationshipData.instances.length > 5 && (
|
{relationshipData.instances.length > 5 && (
|
||||||
<div className={hubStyles.diagramNodeSmall}>
|
<div className={hubStyles.diagramNodeSmall}>
|
||||||
+{relationshipData.instances.length - 5} weitere
|
+{relationshipData.instances.length - 5} {t('weitere')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -491,7 +492,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className={hubStyles.section}>
|
<section className={hubStyles.section}>
|
||||||
<h2 className={hubStyles.sectionTitle}>Feature-Instanzen</h2>
|
<h2 className={hubStyles.sectionTitle}>{t('Feature-Instanzen')}</h2>
|
||||||
{loading && filteredInstances.length === 0 ? (
|
{loading && filteredInstances.length === 0 ? (
|
||||||
<div className={styles.loadingContainer}>
|
<div className={styles.loadingContainer}>
|
||||||
<div className={styles.spinner} />
|
<div className={styles.spinner} />
|
||||||
|
|
@ -502,14 +503,14 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
<FaCube className={styles.emptyIcon} />
|
<FaCube className={styles.emptyIcon} />
|
||||||
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanzen')}</h3>
|
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanzen')}</h3>
|
||||||
<p className={styles.emptyDescription}>
|
<p className={styles.emptyDescription}>
|
||||||
Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.
|
{t('Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.')}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
className={styles.primaryButton}
|
className={styles.primaryButton}
|
||||||
onClick={() => setShowWizard(true)}
|
onClick={() => setShowWizard(true)}
|
||||||
disabled={features.length === 0}
|
disabled={features.length === 0}
|
||||||
>
|
>
|
||||||
+ Erste Instanz erstellen
|
+ {t('Erste Instanz erstellen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -526,8 +527,12 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className={hubStyles.instanceMeta}>
|
<div className={hubStyles.instanceMeta}>
|
||||||
<span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)}</span>
|
<span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)}</span>
|
||||||
<span>{inst.userCount ?? '—'} Benutzer</span>
|
<span>
|
||||||
<span>{inst.roleCount ?? '—'} Rollen</span>
|
{inst.userCount ?? '—'} {t('Benutzer')}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{inst.roleCount ?? '—'} {t('Rollen')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={hubStyles.instanceActions}>
|
<div className={hubStyles.instanceActions}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -535,7 +540,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
className={hubStyles.cardAction}
|
className={hubStyles.cardAction}
|
||||||
onClick={() => handleOpenDetail(inst, selectedMandateId)}
|
onClick={() => handleOpenDetail(inst, selectedMandateId)}
|
||||||
>
|
>
|
||||||
<FaUsers /> Benutzer verwalten
|
<FaUsers /> {t('Benutzer verwalten')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -544,7 +549,7 @@ export const AccessManagementHub: React.FC = () => {
|
||||||
disabled={!inst.enabled}
|
disabled={!inst.enabled}
|
||||||
title={t('Rollen synchronisieren')}
|
title={t('Rollen synchronisieren')}
|
||||||
>
|
>
|
||||||
<FaCogs /> Rollen sync
|
<FaCogs /> {t('Rollen sync')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import { TextField } from '../../components/UiComponents/TextField';
|
||||||
import styles from './Admin.module.css';
|
import styles from './Admin.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
import { labelAsI18nKey } from '../../types/mandate';
|
|
||||||
|
|
||||||
export const AdminFeatureAccessPage: React.FC = () => {
|
export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
@ -94,7 +93,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
render: (value: string) => {
|
render: (value: string) => {
|
||||||
const feature = features.find(f => f.code === value);
|
const feature = features.find(f => f.code === value);
|
||||||
if (feature) {
|
if (feature) {
|
||||||
return t(labelAsI18nKey(feature.label, value));
|
return t(feature.label || value);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +121,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
// Validate label
|
// Validate label
|
||||||
if (!createLabel || createLabel.trim() === '') {
|
if (!createLabel || createLabel.trim() === '') {
|
||||||
showError('Fehler', 'Label ist erforderlich.');
|
showError(t('Fehler'), t('Label ist erforderlich.'));
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +131,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
if (createFeatureCode === 'chatbot') {
|
if (createFeatureCode === 'chatbot') {
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
||||||
showError('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.');
|
showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.'));
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -176,9 +175,9 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
setChatbotAllowedProviders([]);
|
setChatbotAllowedProviders([]);
|
||||||
fetchInstances(selectedMandateId);
|
fetchInstances(selectedMandateId);
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures(); // Refresh global navigation cache
|
||||||
showSuccess('Feature-Instanz erstellt', `Die Instanz "${createLabel}" wurde erfolgreich erstellt.`);
|
showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel }));
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz');
|
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
@ -228,7 +227,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
if (editingInstance.featureCode === 'chatbot') {
|
if (editingInstance.featureCode === 'chatbot') {
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
||||||
showError('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.');
|
showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.'));
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -270,9 +269,9 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
setChatbotAllowedProviders([]);
|
setChatbotAllowedProviders([]);
|
||||||
fetchInstances(selectedMandateId);
|
fetchInstances(selectedMandateId);
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures(); // Refresh global navigation cache
|
||||||
showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`);
|
showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label }));
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Feature-Instanz');
|
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
@ -285,10 +284,10 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
const result = await deleteInstance(selectedMandateId, instanceId);
|
const result = await deleteInstance(selectedMandateId, instanceId);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures(); // Refresh global navigation cache
|
||||||
showSuccess('Instanz gelöscht', 'Die Feature-Instanz wurde gelöscht.');
|
showSuccess(t('Instanz gelöscht'), t('Die Feature-Instanz wurde gelöscht.'));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Löschen der Feature-Instanz');
|
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Feature-Instanz'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -301,11 +300,15 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
showSuccess(
|
showSuccess(
|
||||||
'Rollen synchronisiert',
|
t('Rollen synchronisiert'),
|
||||||
`Hinzugefügt: ${result.data.added}\nEntfernt: ${result.data.removed}\nUnverändert: ${result.data.unchanged}`
|
t('Hinzugefügt: {added}\nEntfernt: {removed}\nUnverändert: {unchanged}', {
|
||||||
|
added: result.data.added,
|
||||||
|
removed: result.data.removed,
|
||||||
|
unchanged: result.data.unchanged,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren der Rollen');
|
showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Rollen'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSyncingInstance(null);
|
setSyncingInstance(null);
|
||||||
|
|
@ -314,18 +317,14 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
|
|
||||||
// Get mandate name
|
// Get mandate name
|
||||||
const getMandateName = (mandate: Mandate) => {
|
const getMandateName = (mandate: Mandate) => {
|
||||||
if (mandate.label) return mandate.label;
|
return mandate.label || mandate.name || mandate.id;
|
||||||
if (typeof mandate.name === 'object') {
|
|
||||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
|
||||||
}
|
|
||||||
return mandate.name || mandate.id;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get feature label
|
// Get feature label
|
||||||
const getFeatureLabel = (code: string) => {
|
const getFeatureLabel = (code: string) => {
|
||||||
const feature = features.find(f => f.code === code);
|
const feature = features.find(f => f.code === code);
|
||||||
if (feature) {
|
if (feature) {
|
||||||
return t(labelAsI18nKey(feature.label, code));
|
return t(feature.label || code);
|
||||||
}
|
}
|
||||||
return code;
|
return code;
|
||||||
};
|
};
|
||||||
|
|
@ -337,7 +336,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
<span className={styles.errorIcon}>⚠️</span>
|
<span className={styles.errorIcon}>⚠️</span>
|
||||||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||||||
<FaSync /> Erneut versuchen
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -348,7 +347,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||||
<div className={styles.pageHeader}>
|
<div className={styles.pageHeader}>
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.pageTitle}>Feature-Instanzen</h1>
|
<h1 className={styles.pageTitle}>{t('Feature-Instanzen')}</h1>
|
||||||
<p className={styles.pageSubtitle}>{t('Verwalten Sie Feature-Instanzen für jeden')}</p>
|
<p className={styles.pageSubtitle}>{t('Verwalten Sie Feature-Instanzen für jeden')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -358,7 +357,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
<div className={styles.filterGroup}>
|
<div className={styles.filterGroup}>
|
||||||
<label className={styles.filterLabel}>
|
<label className={styles.filterLabel}>
|
||||||
<FaBuilding style={{ marginRight: 8 }} />
|
<FaBuilding style={{ marginRight: 8 }} />
|
||||||
Mandant auswählen:
|
{t('Mandant auswählen:')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className={styles.filterSelect}
|
className={styles.filterSelect}
|
||||||
|
|
@ -381,15 +380,19 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
onClick={() => fetchInstances(selectedMandateId)}
|
onClick={() => fetchInstances(selectedMandateId)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.primaryButton}
|
className={styles.primaryButton}
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
disabled={features.length === 0}
|
disabled={features.length === 0}
|
||||||
title={features.length === 0 ? 'Keine Features verfügbar. Bitte laden Sie die Seite neu oder prüfen Sie die Konsole auf Fehler.' : undefined}
|
title={
|
||||||
|
features.length === 0
|
||||||
|
? t('Keine Features verfügbar. Bitte laden Sie die Seite neu oder prüfen Sie die Konsole auf Fehler.')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FaPlus /> Neue Instanz
|
<FaPlus /> {t('Neue Instanz')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -411,16 +414,17 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
<div className={styles.infoBox} style={{ borderColor: 'var(--error-color, #dc3545)', backgroundColor: 'var(--error-bg, rgba(220, 53, 69, 0.1))' }}>
|
<div className={styles.infoBox} style={{ borderColor: 'var(--error-color, #dc3545)', backgroundColor: 'var(--error-bg, rgba(220, 53, 69, 0.1))' }}>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
<span>
|
<span>
|
||||||
Keine Features geladen.
|
{t('Keine Features geladen.')}
|
||||||
{error ? ` Fehler: ${error}` : ' Die API hat keine Features zurückgegeben.'}
|
{error ? ` Fehler: ${error}` : ` ${t('Die API hat keine Features zurückgegeben.')}`}
|
||||||
{' '}Öffnen Sie die Browser-Konsole (F12) und prüfen Sie den Netzwerk-Tab für /api/features/
|
{' '}
|
||||||
|
{t('Öffnen Sie die Browser-Konsole (F12) und prüfen Sie den Netzwerk-Tab für /api/features/')}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => fetchFeatures()}
|
onClick={() => fetchFeatures()}
|
||||||
style={{ marginLeft: '1rem' }}
|
style={{ marginLeft: '1rem' }}
|
||||||
>
|
>
|
||||||
<FaSync /> Features erneut laden
|
<FaSync /> {t('Features erneut laden')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -431,7 +435,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
<FaBuilding className={styles.emptyIcon} />
|
<FaBuilding className={styles.emptyIcon} />
|
||||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||||
<p className={styles.emptyDescription}>
|
<p className={styles.emptyDescription}>
|
||||||
Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.
|
{t('Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -505,12 +509,12 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
{/* Feature Code Selector - Required for chatbot config */}
|
{/* Feature Code Selector - Required for chatbot config */}
|
||||||
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
|
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
|
||||||
<label className={styles.configLabel} style={{ fontWeight: 600 }}>
|
<label className={styles.configLabel} style={{ fontWeight: 600 }}>
|
||||||
Feature auswählen: <span style={{ color: 'var(--error-color)' }}>*</span>
|
{t('Feature auswählen')}: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||||||
</label>
|
</label>
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
items={features.map(f => ({
|
items={features.map(f => ({
|
||||||
id: f.code,
|
id: f.code,
|
||||||
label: t(labelAsI18nKey(f.label, f.code)),
|
label: t(f.label || f.code),
|
||||||
value: f.code
|
value: f.code
|
||||||
}))}
|
}))}
|
||||||
selectedItemId={createFeatureCode}
|
selectedItemId={createFeatureCode}
|
||||||
|
|
@ -528,7 +532,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
{!createFeatureCode && (
|
{!createFeatureCode && (
|
||||||
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
|
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
|
||||||
Bitte wählen Sie ein Feature aus, um fortzufahren.
|
{t('Bitte wählen Sie ein Feature aus, um fortzufahren.')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -536,7 +540,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
{/* Chatbot Configuration Title - Show when chatbot is selected */}
|
{/* Chatbot Configuration Title - Show when chatbot is selected */}
|
||||||
{createFeatureCode === 'chatbot' && (
|
{createFeatureCode === 'chatbot' && (
|
||||||
<h3 className={styles.configSectionTitle} style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
|
<h3 className={styles.configSectionTitle} style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
|
||||||
Chatbot-Konfiguration
|
{t('Chatbot-Konfiguration')}
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -544,7 +548,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
{createFeatureCode && (
|
{createFeatureCode && (
|
||||||
<div className={styles.configField} style={{ marginBottom: '1.5rem' }}>
|
<div className={styles.configField} style={{ marginBottom: '1.5rem' }}>
|
||||||
<label className={styles.configLabel}>
|
<label className={styles.configLabel}>
|
||||||
Label: <span style={{ color: 'var(--error-color)' }}>*</span>
|
{t('Label')}: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||||||
</label>
|
</label>
|
||||||
<TextField
|
<TextField
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import api from '../../api';
|
||||||
import styles from './Admin.module.css';
|
import styles from './Admin.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
import { labelAsI18nKey } from '../../types/mandate';
|
|
||||||
|
|
||||||
export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
@ -93,7 +92,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
allOptions.push({
|
allOptions.push({
|
||||||
mandateId: mandate.id,
|
mandateId: mandate.id,
|
||||||
instanceId: inst.id,
|
instanceId: inst.id,
|
||||||
mandateName: mandate.label || (typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id)),
|
mandateName: mandate.label || mandate.name || mandate.id,
|
||||||
instanceLabel: inst.label || inst.id,
|
instanceLabel: inst.label || inst.id,
|
||||||
featureCode: inst.featureCode,
|
featureCode: inst.featureCode,
|
||||||
combinedKey: `${mandate.id}:${inst.id}`,
|
combinedKey: `${mandate.id}:${inst.id}`,
|
||||||
|
|
@ -314,9 +313,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
setShowAddModal(false);
|
setShowAddModal(false);
|
||||||
refreshUsers();
|
refreshUsers();
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures(); // Refresh global navigation cache
|
||||||
showSuccess('Benutzer hinzugefügt', 'Der Benutzer wurde erfolgreich zur Feature-Instanz hinzugefügt.');
|
showSuccess(t('Benutzer hinzugefügt'), t('Der Benutzer wurde erfolgreich zur Feature-Instanz hinzugefügt.'));
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers');
|
showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
@ -338,9 +337,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
refreshUsers();
|
refreshUsers();
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures(); // Refresh global navigation cache
|
||||||
showSuccess('Eintrag aktualisiert', 'Rollen und Aktiv-Status wurden erfolgreich aktualisiert.');
|
showSuccess(t('Eintrag aktualisiert'), t('Rollen und Aktiv-Status wurden erfolgreich aktualisiert.'));
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Aktualisieren');
|
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
@ -354,9 +353,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
refreshUsers();
|
refreshUsers();
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures(); // Refresh global navigation cache
|
||||||
showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`);
|
showSuccess(t('Benutzer entfernt'), t('"{name}" wurde aus der Feature-Instanz entfernt.', { name: user.username }));
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
|
showError(t('Fehler'), result.error || t('Fehler beim Entfernen des Benutzers'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -369,7 +368,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
const getFeatureLabel = (code: string) => {
|
const getFeatureLabel = (code: string) => {
|
||||||
const feature = features.find(f => f.code === code);
|
const feature = features.find(f => f.code === code);
|
||||||
if (feature) {
|
if (feature) {
|
||||||
return t(labelAsI18nKey(feature.label, code));
|
return t(feature.label || code);
|
||||||
}
|
}
|
||||||
return code;
|
return code;
|
||||||
};
|
};
|
||||||
|
|
@ -396,9 +395,11 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<span className={styles.errorIcon}>⚠️</span>
|
<span className={styles.errorIcon}>⚠️</span>
|
||||||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
<p className={styles.errorMessage}>
|
||||||
|
{t('Fehler')}: {error}
|
||||||
|
</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||||||
<FaSync /> Erneut versuchen
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -419,7 +420,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
<div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}>
|
<div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}>
|
||||||
<label className={styles.filterLabel}>
|
<label className={styles.filterLabel}>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
Mandant / Feature-Instanz:
|
{t('Mandant / Feature-Instanz')}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className={styles.filterSelect}
|
className={styles.filterSelect}
|
||||||
|
|
@ -458,14 +459,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
onClick={() => refreshUsers()}
|
onClick={() => refreshUsers()}
|
||||||
disabled={usersLoading}
|
disabled={usersLoading}
|
||||||
>
|
>
|
||||||
<FaSync className={usersLoading ? 'spinning' : ''} /> Aktualisieren
|
<FaSync className={usersLoading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.primaryButton}
|
className={styles.primaryButton}
|
||||||
onClick={() => setShowAddModal(true)}
|
onClick={() => setShowAddModal(true)}
|
||||||
disabled={availableUsers.length === 0 || instanceRoles.length === 0}
|
disabled={availableUsers.length === 0 || instanceRoles.length === 0}
|
||||||
>
|
>
|
||||||
<FaPlus /> Benutzer hinzufügen
|
<FaPlus /> {t('Benutzer hinzufügen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -475,10 +476,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
{selectedOption && (
|
{selectedOption && (
|
||||||
<div className={styles.infoBox}>
|
<div className={styles.infoBox}>
|
||||||
<FaBuilding style={{ marginRight: 8 }} />
|
<FaBuilding style={{ marginRight: 8 }} />
|
||||||
<span>Mandant: <strong>{selectedOption.mandateName}</strong></span>
|
<span>
|
||||||
|
{t('Mandant')}: <strong>{selectedOption.mandateName}</strong>
|
||||||
|
</span>
|
||||||
<span style={{ margin: '0 16px', color: 'var(--color-border)' }}>|</span>
|
<span style={{ margin: '0 16px', color: 'var(--color-border)' }}>|</span>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
<span>Instanz: <strong>{selectedOption.instanceLabel}</strong> ({selectedOption.featureCode})</span>
|
<span>
|
||||||
|
{t('Instanz')}: <strong>{selectedOption.instanceLabel}</strong> ({selectedOption.featureCode})
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -497,7 +502,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
|
|
||||||
{/* Warning if no roles available */}
|
{/* Warning if no roles available */}
|
||||||
{selectedInstance && instanceRoles.length === 0 && !usersLoading && (
|
{selectedInstance && instanceRoles.length === 0 && !usersLoading && (
|
||||||
<div className={styles.warningBox || styles.infoBox}>
|
<div className={styles.infoBox} style={{ borderColor: 'var(--warning-color, #d69e2e)', backgroundColor: 'var(--warning-bg, rgba(214, 158, 46, 0.12))' }}>
|
||||||
<span>⚠️ </span>
|
<span>⚠️ </span>
|
||||||
<span>{t('Diese Instanz hat noch keine')}</span>
|
<span>{t('Diese Instanz hat noch keine')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -595,7 +600,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
|
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
|
||||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||||
<div className={styles.modalHeader}>
|
<div className={styles.modalHeader}>
|
||||||
<h2 className={styles.modalTitle}>Rollen bearbeiten: {editingUser.username}</h2>
|
<h2 className={styles.modalTitle}>
|
||||||
|
{t('Rollen bearbeiten')}: {editingUser.username}
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
className={styles.modalClose}
|
className={styles.modalClose}
|
||||||
onClick={() => setEditingUser(null)}
|
onClick={() => setEditingUser(null)}
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,18 @@ import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
|
|
||||||
interface Feature {
|
interface Feature {
|
||||||
id?: string;
|
id?: string;
|
||||||
code: string; // Backend uses 'code' not 'featureCode'
|
code: string;
|
||||||
featureCode?: string; // Alias for backward compatibility
|
featureCode?: string;
|
||||||
label: string | { [key: string]: string }; // Backend uses 'label' not 'name'
|
label: string;
|
||||||
name?: string | { [key: string]: string }; // Alias for backward compatibility
|
name?: string;
|
||||||
description?: string | { [key: string]: string };
|
description?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeatureRole {
|
interface FeatureRole {
|
||||||
id: string;
|
id: string;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
description?: { [key: string]: string };
|
description?: string;
|
||||||
featureCode: string;
|
featureCode: string;
|
||||||
mandateId?: string | null;
|
mandateId?: string | null;
|
||||||
featureInstanceId?: string | null;
|
featureInstanceId?: string | null;
|
||||||
|
|
@ -118,17 +118,14 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedFeatureCode]);
|
}, [selectedFeatureCode, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}, [fetchRoles]);
|
}, [fetchRoles]);
|
||||||
|
|
||||||
// Get text from multilingual object
|
const getTextValue = (value: string | undefined): string => {
|
||||||
const getTextValue = (value: string | { [key: string]: string } | undefined): string => {
|
return value || '-';
|
||||||
if (!value) return '-';
|
|
||||||
if (typeof value === 'string') return value;
|
|
||||||
return value.de || value.en || Object.values(value)[0] || '-';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Table columns
|
// Table columns
|
||||||
|
|
@ -148,7 +145,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
width: 300,
|
width: 300,
|
||||||
formatter: (value: string | { [key: string]: string }) => getTextValue(value)
|
formatter: (value: string) => getTextValue(value)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'featureCode',
|
key: 'featureCode',
|
||||||
|
|
@ -178,9 +175,9 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
label: t('Beschreibung'),
|
label: t('Beschreibung'),
|
||||||
type: 'multilingual',
|
type: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
description: t('Mehrsprachige Beschreibung')
|
description: t('Beschreibung der Rolle')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
return fields;
|
return fields;
|
||||||
|
|
@ -200,15 +197,15 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
label: t('Beschreibung'),
|
label: t('Beschreibung'),
|
||||||
type: 'multilingual',
|
type: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
description: t('Mehrsprachige Beschreibung')
|
description: t('Beschreibung der Rolle')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
// Handle create role
|
// Handle create role
|
||||||
const handleCreateRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => {
|
const handleCreateRole = async (data: { roleLabel: string; description?: string }) => {
|
||||||
if (!selectedFeatureCode) return;
|
if (!selectedFeatureCode) return;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -216,20 +213,20 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
params.append('roleLabel', data.roleLabel);
|
params.append('roleLabel', data.roleLabel);
|
||||||
params.append('featureCode', selectedFeatureCode);
|
params.append('featureCode', selectedFeatureCode);
|
||||||
|
|
||||||
await api.post(`/api/features/templates/roles?${params.toString()}`, data.description || {});
|
await api.post(`/api/features/templates/roles?${params.toString()}`, data.description ?? '');
|
||||||
|
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
await fetchRoles();
|
await fetchRoles();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error creating role:', err);
|
console.error('Error creating role:', err);
|
||||||
showError('Fehler', err.response?.data?.detail || 'Fehler beim Erstellen der Rolle');
|
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Erstellen der Rolle'));
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle edit role
|
// Handle edit role
|
||||||
const handleEditRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => {
|
const handleEditRole = async (data: { roleLabel: string; description?: string }) => {
|
||||||
if (!editingRole) return;
|
if (!editingRole) return;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -241,7 +238,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
await fetchRoles();
|
await fetchRoles();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error updating role:', err);
|
console.error('Error updating role:', err);
|
||||||
showError('Fehler', err.response?.data?.detail || 'Fehler beim Aktualisieren der Rolle');
|
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Aktualisieren der Rolle'));
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +251,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
await fetchRoles();
|
await fetchRoles();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error deleting role:', err);
|
console.error('Error deleting role:', err);
|
||||||
showError('Fehler', err.response?.data?.detail || 'Fehler beim Löschen der Rolle');
|
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Löschen der Rolle'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -276,7 +273,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<span className={styles.errorIcon}>⚠️</span>
|
<span className={styles.errorIcon}>⚠️</span>
|
||||||
<p className={styles.errorMessage}>{error}</p>
|
<p className={styles.errorMessage}>{error}</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => window.location.reload()}>
|
<button className={styles.secondaryButton} onClick={() => window.location.reload()}>
|
||||||
<FaSync /> Erneut versuchen
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -297,7 +294,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<div className={styles.filterGroup}>
|
<div className={styles.filterGroup}>
|
||||||
<label className={styles.filterLabel}>
|
<label className={styles.filterLabel}>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
Feature:
|
{t('Feature:')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className={styles.filterSelect}
|
className={styles.filterSelect}
|
||||||
|
|
@ -323,13 +320,13 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
onClick={() => fetchRoles()}
|
onClick={() => fetchRoles()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.primaryButton}
|
className={styles.primaryButton}
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
>
|
>
|
||||||
<FaPlus /> Neue Feature-Rolle
|
<FaPlus /> {t('Neue Feature-Rolle')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -340,8 +337,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<div className={styles.infoBox}>
|
<div className={styles.infoBox}>
|
||||||
<FaUserShield style={{ marginRight: 8 }} />
|
<FaUserShield style={{ marginRight: 8 }} />
|
||||||
<span>
|
<span>
|
||||||
<strong>Feature-Template-Rollen</strong> werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert.
|
<strong>{t('Feature-Template-Rollen')}</strong>{' '}
|
||||||
Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus.
|
{t('werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert. Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus.')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -352,7 +349,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<FaCube className={styles.emptyIcon} />
|
<FaCube className={styles.emptyIcon} />
|
||||||
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
|
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
|
||||||
<p className={styles.emptyDescription}>
|
<p className={styles.emptyDescription}>
|
||||||
Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.
|
{t('Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -414,14 +411,16 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
<span>Feature: <strong>{selectedFeatureCode}</strong></span>
|
<span>
|
||||||
|
{t('Feature')}: <strong>{selectedFeatureCode}</strong>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<FormGeneratorForm
|
<FormGeneratorForm
|
||||||
attributes={createFields}
|
attributes={createFields}
|
||||||
mode="create"
|
mode="create"
|
||||||
onSubmit={handleCreateRole}
|
onSubmit={handleCreateRole}
|
||||||
onCancel={() => setShowCreateModal(false)}
|
onCancel={() => setShowCreateModal(false)}
|
||||||
submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'}
|
submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')}
|
||||||
cancelButtonText={t('Abbrechen')}
|
cancelButtonText={t('Abbrechen')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -445,7 +444,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
<span>Feature: <strong>{editingRole.featureCode}</strong></span>
|
<span>{t('Feature:')} <strong>{editingRole.featureCode}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<FormGeneratorForm
|
<FormGeneratorForm
|
||||||
attributes={editFields}
|
attributes={editFields}
|
||||||
|
|
@ -468,7 +467,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<div className={styles.modalHeader}>
|
<div className={styles.modalHeader}>
|
||||||
<h2 className={styles.modalTitle}>
|
<h2 className={styles.modalTitle}>
|
||||||
<FaShieldAlt style={{ marginRight: 8 }} />
|
<FaShieldAlt style={{ marginRight: 8 }} />
|
||||||
Berechtigungen: {permissionsRole.roleLabel}
|
{t('Berechtigungen')}: {permissionsRole.roleLabel}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
className={styles.modalClose}
|
className={styles.modalClose}
|
||||||
|
|
@ -480,7 +479,9 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||||
<FaCube style={{ marginRight: 8 }} />
|
<FaCube style={{ marginRight: 8 }} />
|
||||||
<span>Feature: <strong>{permissionsRole.featureCode}</strong></span>
|
<span>
|
||||||
|
{t('Feature')}: <strong>{permissionsRole.featureCode}</strong>
|
||||||
|
</span>
|
||||||
<span style={{ marginLeft: '1rem' }}>{t('Template-Rolle global')}</span>
|
<span style={{ marginLeft: '1rem' }}>{t('Template-Rolle global')}</span>
|
||||||
</div>
|
</div>
|
||||||
<AccessRulesEditor
|
<AccessRulesEditor
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue