From 1c4233c7ea52409ea8c1c006a706de3347a0eb98 Mon Sep 17 00:00:00 2001 From: ValueOn AG
+ {t('Liest aus dem lokalen Mirror. Tree-Layout und Editor-Pane folgen im naechsten Schritt.')} +
+ {t('(Anzeige auf 200 begrenzt -- Tree-Layout folgt.)')} +
+ {t('Verbindung dieser Feature-Instanz zu einem Redmine-Projekt. Speichern, testen, dann initialen Sync starten.')} +
+ {t('Tickets werden in die lokale Datenbank gespiegelt, damit Statistik und Browser auch bei 20\u2019000+ Tickets schnell sind. Nach Aenderungen wird das Mirror-Bild automatisch nachgezogen.')} +
+ {t('Aggregiert aus dem lokalen Mirror. PeriodPicker und FormGeneratorReport folgen im naechsten Schritt.')} +
- {t('Liest aus dem lokalen Mirror. Tree-Layout und Editor-Pane folgen im naechsten Schritt.')} -
+ {t('Baum aus dem lokalen Mirror. Roots: {name}. Tickets ohne Verbindung landen unter "Orphan User Story".', { + name: schema?.rootTrackerName || config?.rootTrackerName || '—', + })} +
- {t('(Anzeige auf 200 begrenzt -- Tree-Layout folgt.)')} -
- {t('Aggregiert aus dem lokalen Mirror. PeriodPicker und FormGeneratorReport folgen im naechsten Schritt.')} + {t('Aggregiert aus dem lokalen Mirror. Filter werden serverseitig angewendet; Zeitraum steuert auch die "im Zeitraum"-KPIs.')}
{cfg.description}
{loginValue}
{pwd}
{cred.email}
{expectedType ?? '?'}
+ {t('Authentifizierte Teams-Konten, mit denen der Bot Meetings beitreten kann. Passwoerter werden verschluesselt gespeichert und nie zurueckgegeben.')} +
+ * + * Behavior matches the rest of the editor: + * - 0 results -> hint to create a feature instance for this mandate + * - 1 result -> auto-pick (no manual click required) + * - N results -> + * + * The bound value is a plain `` string so backend adapters can keep + * using `featureInstanceId` lookups unchanged. Type stays + * `FeatureInstanceRef[]` on the parameter so DataPicker / RequiredAttributePicker + * filter correctly. + */ + +import React from 'react'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import type { FieldRendererProps } from './index'; + +type FeatureInstanceOption = { id: string; label: string }; + +export const FeatureInstancePicker: React.FC = ({ + param, + value, + onChange, + instanceId, + request, +}) => { + const { t } = useLanguage(); + const featureCode = + (param.frontendOptions?.featureCode as string | undefined) || undefined; + const [instances, setInstances] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [loadError, setLoadError] = React.useState(null); + const autoSingleRef = React.useRef(false); + + React.useEffect(() => { + if (!instanceId || !request || !featureCode) return; + setLoading(true); + setLoadError(null); + request({ + url: `/api/workflows/${instanceId}/options/feature.instance?featureCode=${encodeURIComponent(featureCode)}`, + method: 'get', + }) + .then((res: unknown) => { + const data = res as { options?: Array<{ value: string; label: string }> }; + setInstances((data?.options || []).map((o) => ({ id: o.value, label: o.label }))); + }) + .catch((err: unknown) => { + console.error('FeatureInstancePicker: failed to load instances', err); + setInstances([]); + setLoadError(err instanceof Error ? err.message : String(err)); + }) + .finally(() => setLoading(false)); + }, [instanceId, request, featureCode]); + + React.useEffect(() => { + if (instances.length !== 1 || autoSingleRef.current) return; + if (value !== '' && value !== undefined && value !== null) return; + autoSingleRef.current = true; + onChange(instances[0].id); + }, [instances, value, onChange]); + + const strVal = typeof value === 'string' ? value : ''; + const codeLabel = featureCode ?? t('Feature'); + + return ( + + + {param.description || param.name} + + {loading && ( + {t('Lade…')} + )} + {!loading && instances.length === 0 && !loadError && ( + + {t('Keine {code}-Instanz im aktiven Mandanten — bitte in der Admin-Konsole anlegen.', { code: codeLabel })} + + )} + {!loading && instances.length === 1 && ( + + {instances[0].label} + + )} + {!loading && instances.length > 1 && ( + onChange(e.target.value)} + style={{ + width: '100%', + maxWidth: '100%', + boxSizing: 'border-box', + padding: '4px 8px', + borderRadius: 4, + border: '1px solid var(--border-color)', + background: 'var(--bg-primary)', + color: 'var(--text-primary)', + }} + > + {t('{code}-Mandant wählen', { code: codeLabel })} + {instances.map((c) => ( + {c.label} + ))} + + )} + {loadError && ( + + {t('Mandanten-Liste konnte nicht geladen werden')} + + )} + + ); +}; + +export default FeatureInstancePicker; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx index a910ca6..fb33a03 100644 --- a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -31,6 +31,7 @@ import { toApiGraph } from '../shared/graphUtils'; import { postUpstreamPaths } from '../../../../api/workflowApi'; import type { CanvasNode } from '../../editor/FlowCanvas'; import { DataRefRenderer } from './DataRefRenderer'; +import { FeatureInstancePicker } from './FeatureInstancePicker'; const TextInput: React.FC = ({ param, value, onChange }) => ( @@ -755,6 +756,7 @@ export const FRONTEND_TYPE_RENDERERS: Record = { hidden: HiddenInput, dataRef: DataRefRenderer, userConnection: ConnectionPicker, + featureInstance: FeatureInstancePicker, sharepointFolder: SharepointPathPicker, sharepointFile: SharepointPathPicker, clickupList: FolderPicker, diff --git a/src/components/FlowEditor/nodes/shared/DataPicker.tsx b/src/components/FlowEditor/nodes/shared/DataPicker.tsx index c1ea657..4edadc7 100644 --- a/src/components/FlowEditor/nodes/shared/DataPicker.tsx +++ b/src/components/FlowEditor/nodes/shared/DataPicker.tsx @@ -6,6 +6,7 @@ */ import React, { useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import type { GraphDefinedSchemaRef, NodeType, PortSchema } from '../../../../api/workflowApi'; @@ -254,33 +255,35 @@ export const DataPicker: React.FC = ({ open, onClose(); }; - return ( - - e.stopPropagation()}> + const _dialog = ( + e.key === 'Escape' && onClose()} + role="presentation" + > + e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="automation2DataPickerTitle" + > - + {t('Datenquelle wählen')} {expectedParamType && ( {expectedParamType} )} - + {expectedParamType && ( - + = ({ open, ]; return ( - {loopLabel} + {loopLabel} {loopPaths.map((p, i) => { const compat = expectedParamType && p.type ? isCompatible(p.type, expectedParamType) @@ -330,7 +333,7 @@ export const DataPicker: React.FC = ({ open, > {p.label} {p.type && ( - + ({p.type}) )} @@ -364,7 +367,7 @@ export const DataPicker: React.FC = ({ open, onClick={() => handlePickSystemVar(key)} title={info.description} > - {key} ({info.type}) + {key} ({info.type}) ))} @@ -418,7 +421,7 @@ export const DataPicker: React.FC = ({ open, {isExpanded ? '▼' : '▶'} {label} {resolvedSchema && ( - + ({resolvedSchema.name}) )} @@ -426,7 +429,7 @@ export const DataPicker: React.FC = ({ open, {isExpanded && ( {paths.length === 0 && ( - + {t('(keine kompatiblen Felder — Filter „Nur kompatible“ deaktivieren)')} )} @@ -446,7 +449,7 @@ export const DataPicker: React.FC = ({ open, > {p.label} {p.type && ( - + ({p.type}) )} @@ -454,15 +457,8 @@ export const DataPicker: React.FC = ({ open, {p.iterable && ( handlePickIterate(nodeId, p.path, expectedParamType)} - style={{ - fontSize: 10, - padding: '2px 6px', - background: 'rgba(0,123,255,0.10)', - color: 'var(--primary-color, #007bff)', - whiteSpace: 'nowrap', - }} title={t('Pro Element der Liste iterieren (Loop)')} > {t('iterieren')} @@ -481,4 +477,6 @@ export const DataPicker: React.FC = ({ open, ); + + return createPortal(_dialog, document.body); }; diff --git a/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx b/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx index 81236c4..13d84c4 100644 --- a/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx +++ b/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx @@ -91,9 +91,30 @@ export const RequiredAttributePicker: React.FC = ( }; return ( - - - + + {/* Header: label always takes the full row (flex-basis 100 %), badge + wraps below — prevents long type names like List[ActionDocument] + from escaping the panel frame on the right. */} + + {label} * @@ -103,10 +124,15 @@ export const RequiredAttributePicker: React.FC = ( style={{ fontSize: 10, fontFamily: 'monospace', - color: '#555', - background: '#eee', + color: 'var(--text-secondary, #555)', + background: 'var(--bg-secondary, #eee)', + border: '1px solid var(--border-color, #e0e0e0)', borderRadius: 4, padding: '1px 6px', + maxWidth: '100%', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', }} > {expectedType} @@ -115,8 +141,9 @@ export const RequiredAttributePicker: React.FC = ( {isBoundRef ? ( - + = ( color: 'var(--success-color, #28a745)', fontSize: 12, fontWeight: 500, + maxWidth: '100%', + overflowWrap: 'anywhere', + wordBreak: 'break-word', + lineHeight: 1.4, }} > {boundLabel} @@ -132,7 +163,7 @@ export const RequiredAttributePicker: React.FC
]` on the parameter so DataPicker / RequiredAttributePicker + * filter correctly. + */ + +import React from 'react'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import type { FieldRendererProps } from './index'; + +type FeatureInstanceOption = { id: string; label: string }; + +export const FeatureInstancePicker: React.FC = ({ + param, + value, + onChange, + instanceId, + request, +}) => { + const { t } = useLanguage(); + const featureCode = + (param.frontendOptions?.featureCode as string | undefined) || undefined; + const [instances, setInstances] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [loadError, setLoadError] = React.useState(null); + const autoSingleRef = React.useRef(false); + + React.useEffect(() => { + if (!instanceId || !request || !featureCode) return; + setLoading(true); + setLoadError(null); + request({ + url: `/api/workflows/${instanceId}/options/feature.instance?featureCode=${encodeURIComponent(featureCode)}`, + method: 'get', + }) + .then((res: unknown) => { + const data = res as { options?: Array<{ value: string; label: string }> }; + setInstances((data?.options || []).map((o) => ({ id: o.value, label: o.label }))); + }) + .catch((err: unknown) => { + console.error('FeatureInstancePicker: failed to load instances', err); + setInstances([]); + setLoadError(err instanceof Error ? err.message : String(err)); + }) + .finally(() => setLoading(false)); + }, [instanceId, request, featureCode]); + + React.useEffect(() => { + if (instances.length !== 1 || autoSingleRef.current) return; + if (value !== '' && value !== undefined && value !== null) return; + autoSingleRef.current = true; + onChange(instances[0].id); + }, [instances, value, onChange]); + + const strVal = typeof value === 'string' ? value : ''; + const codeLabel = featureCode ?? t('Feature'); + + return ( + + + {param.description || param.name} + + {loading && ( + {t('Lade…')} + )} + {!loading && instances.length === 0 && !loadError && ( + + {t('Keine {code}-Instanz im aktiven Mandanten — bitte in der Admin-Konsole anlegen.', { code: codeLabel })} + + )} + {!loading && instances.length === 1 && ( + + {instances[0].label} + + )} + {!loading && instances.length > 1 && ( + onChange(e.target.value)} + style={{ + width: '100%', + maxWidth: '100%', + boxSizing: 'border-box', + padding: '4px 8px', + borderRadius: 4, + border: '1px solid var(--border-color)', + background: 'var(--bg-primary)', + color: 'var(--text-primary)', + }} + > + {t('{code}-Mandant wählen', { code: codeLabel })} + {instances.map((c) => ( + {c.label} + ))} + + )} + {loadError && ( + + {t('Mandanten-Liste konnte nicht geladen werden')} + + )} + + ); +}; + +export default FeatureInstancePicker; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx index a910ca6..fb33a03 100644 --- a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -31,6 +31,7 @@ import { toApiGraph } from '../shared/graphUtils'; import { postUpstreamPaths } from '../../../../api/workflowApi'; import type { CanvasNode } from '../../editor/FlowCanvas'; import { DataRefRenderer } from './DataRefRenderer'; +import { FeatureInstancePicker } from './FeatureInstancePicker'; const TextInput: React.FC = ({ param, value, onChange }) => ( @@ -755,6 +756,7 @@ export const FRONTEND_TYPE_RENDERERS: Record = { hidden: HiddenInput, dataRef: DataRefRenderer, userConnection: ConnectionPicker, + featureInstance: FeatureInstancePicker, sharepointFolder: SharepointPathPicker, sharepointFile: SharepointPathPicker, clickupList: FolderPicker, diff --git a/src/components/FlowEditor/nodes/shared/DataPicker.tsx b/src/components/FlowEditor/nodes/shared/DataPicker.tsx index c1ea657..4edadc7 100644 --- a/src/components/FlowEditor/nodes/shared/DataPicker.tsx +++ b/src/components/FlowEditor/nodes/shared/DataPicker.tsx @@ -6,6 +6,7 @@ */ import React, { useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import type { GraphDefinedSchemaRef, NodeType, PortSchema } from '../../../../api/workflowApi'; @@ -254,33 +255,35 @@ export const DataPicker: React.FC = ({ open, onClose(); }; - return ( - - e.stopPropagation()}> + const _dialog = ( + e.key === 'Escape' && onClose()} + role="presentation" + > + e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="automation2DataPickerTitle" + > - + {t('Datenquelle wählen')} {expectedParamType && ( {expectedParamType} )} - + {expectedParamType && ( - + = ({ open, ]; return ( - {loopLabel} + {loopLabel} {loopPaths.map((p, i) => { const compat = expectedParamType && p.type ? isCompatible(p.type, expectedParamType) @@ -330,7 +333,7 @@ export const DataPicker: React.FC = ({ open, > {p.label} {p.type && ( - + ({p.type}) )} @@ -364,7 +367,7 @@ export const DataPicker: React.FC = ({ open, onClick={() => handlePickSystemVar(key)} title={info.description} > - {key} ({info.type}) + {key} ({info.type}) ))} @@ -418,7 +421,7 @@ export const DataPicker: React.FC = ({ open, {isExpanded ? '▼' : '▶'} {label} {resolvedSchema && ( - + ({resolvedSchema.name}) )} @@ -426,7 +429,7 @@ export const DataPicker: React.FC = ({ open, {isExpanded && ( {paths.length === 0 && ( - + {t('(keine kompatiblen Felder — Filter „Nur kompatible“ deaktivieren)')} )} @@ -446,7 +449,7 @@ export const DataPicker: React.FC = ({ open, > {p.label} {p.type && ( - + ({p.type}) )} @@ -454,15 +457,8 @@ export const DataPicker: React.FC = ({ open, {p.iterable && ( handlePickIterate(nodeId, p.path, expectedParamType)} - style={{ - fontSize: 10, - padding: '2px 6px', - background: 'rgba(0,123,255,0.10)', - color: 'var(--primary-color, #007bff)', - whiteSpace: 'nowrap', - }} title={t('Pro Element der Liste iterieren (Loop)')} > {t('iterieren')} @@ -481,4 +477,6 @@ export const DataPicker: React.FC = ({ open, ); + + return createPortal(_dialog, document.body); }; diff --git a/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx b/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx index 81236c4..13d84c4 100644 --- a/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx +++ b/src/components/FlowEditor/nodes/shared/RequiredAttributePicker.tsx @@ -91,9 +91,30 @@ export const RequiredAttributePicker: React.FC = ( }; return ( - - - + + {/* Header: label always takes the full row (flex-basis 100 %), badge + wraps below — prevents long type names like List[ActionDocument] + from escaping the panel frame on the right. */} + + {label} * @@ -103,10 +124,15 @@ export const RequiredAttributePicker: React.FC = ( style={{ fontSize: 10, fontFamily: 'monospace', - color: '#555', - background: '#eee', + color: 'var(--text-secondary, #555)', + background: 'var(--bg-secondary, #eee)', + border: '1px solid var(--border-color, #e0e0e0)', borderRadius: 4, padding: '1px 6px', + maxWidth: '100%', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', }} > {expectedType} @@ -115,8 +141,9 @@ export const RequiredAttributePicker: React.FC = ( {isBoundRef ? ( - + = ( color: 'var(--success-color, #28a745)', fontSize: 12, fontWeight: 500, + maxWidth: '100%', + overflowWrap: 'anywhere', + wordBreak: 'break-word', + lineHeight: 1.4, }} > {boundLabel} @@ -132,7 +163,7 @@ export const RequiredAttributePicker: React.FC