feat: extract content node angepasst für mehr optionen
This commit is contained in:
parent
0fd05f638f
commit
587dad5cf9
2 changed files with 429 additions and 83 deletions
|
|
@ -15,6 +15,82 @@ import { useAutomation2DataFlow } from '../context/Automation2DataFlowContext';
|
||||||
import styles from './Automation2FlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
import { AccordionList } from '../../UiComponents/AccordionList';
|
||||||
|
import type { AccordionListItem } from '../../UiComponents/AccordionList';
|
||||||
|
|
||||||
|
const CONTEXT_EXTRACT_CONTENT_NODE_TYPE = 'context.extractContent';
|
||||||
|
const CONTEXT_EXTRACT_CHUNK_PARAM_NAMES = ['chunkSizeUnit', 'chunkSize', 'chunkOverlap'] as const;
|
||||||
|
const CONTEXT_EXTRACT_CHUNK_SET = new Set<string>(CONTEXT_EXTRACT_CHUNK_PARAM_NAMES);
|
||||||
|
|
||||||
|
/** Optional params use stored value only (unset ⇒ no chip). Required uses schema default as fallback. */
|
||||||
|
export function workflowParamUiValue(stored: Record<string, unknown>, param: NodeTypeParameter): unknown {
|
||||||
|
const raw = stored[param.name];
|
||||||
|
if (param.required) {
|
||||||
|
return raw !== undefined && raw !== null ? raw : param.default;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveSchemaParamString(name: string, currentParams: Record<string, unknown>, nt: NodeType): string {
|
||||||
|
const raw = currentParams[name];
|
||||||
|
const s = raw !== undefined && raw !== null ? String(raw) : '';
|
||||||
|
if (s !== '') return s;
|
||||||
|
const meta = nt.parameters?.find((p) => p.name === name);
|
||||||
|
const d = meta?.default;
|
||||||
|
return d !== undefined && d !== null ? String(d) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function accordionExtractParamTitle(param: NodeTypeParameter, t: (key: string) => string): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<span style={{ fontWeight: 700, fontSize: 12 }}>
|
||||||
|
{param.required ? (
|
||||||
|
<span style={{ color: 'var(--danger-color, #dc3545)', marginRight: 3 }} title={t('Pflichtfeld')}>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{param.name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verboseSchemaTypeBadge(
|
||||||
|
verboseSchema: boolean,
|
||||||
|
param: NodeTypeParameter,
|
||||||
|
t: (key: string) => string,
|
||||||
|
): React.ReactElement | null {
|
||||||
|
if (!verboseSchema || !param.type) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
marginBottom: 6,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
title={t('Parameter-Typ')}
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border-color)',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: '1px 6px',
|
||||||
|
maxWidth: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{param.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface NodeConfigPanelProps {
|
interface NodeConfigPanelProps {
|
||||||
node: CanvasNode | null;
|
node: CanvasNode | null;
|
||||||
|
|
@ -30,6 +106,35 @@ interface NodeConfigPanelProps {
|
||||||
verboseSchema?: boolean;
|
verboseSchema?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** When ``frontendOptions.dependsOn`` and ``frontendOptions.showWhen`` are set
|
||||||
|
* (same convention as trustee / gateway nodeAdapter ``visibleWhen``), hide the
|
||||||
|
* parameter unless the referenced parameter's effective value matches.
|
||||||
|
*/
|
||||||
|
export function parameterVisibleForFrontendOptions(
|
||||||
|
param: NodeTypeParameter,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
nodeType: NodeType,
|
||||||
|
): boolean {
|
||||||
|
const fo = param.frontendOptions;
|
||||||
|
if (!fo || typeof fo !== 'object') return true;
|
||||||
|
const dependsOnRaw = fo.dependsOn as unknown;
|
||||||
|
const showWhenRaw = fo.showWhen as unknown;
|
||||||
|
if (typeof dependsOnRaw !== 'string' || dependsOnRaw.length === 0 || showWhenRaw === undefined || showWhenRaw === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const depMeta = nodeType.parameters?.find((p) => p.name === dependsOnRaw);
|
||||||
|
const rawSibling = params[dependsOnRaw];
|
||||||
|
const siblingValue =
|
||||||
|
rawSibling !== undefined && rawSibling !== null ? String(rawSibling) : '';
|
||||||
|
const fallback =
|
||||||
|
depMeta?.default !== undefined && depMeta?.default !== null ? String(depMeta.default) : '';
|
||||||
|
const effective = siblingValue !== '' ? siblingValue : fallback;
|
||||||
|
const allowed: string[] = Array.isArray(showWhenRaw)
|
||||||
|
? showWhenRaw.map((x) => String(x))
|
||||||
|
: [String(showWhenRaw)];
|
||||||
|
return allowed.includes(effective);
|
||||||
|
}
|
||||||
|
|
||||||
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
nodeType,
|
nodeType,
|
||||||
language,
|
language,
|
||||||
|
|
@ -62,7 +167,12 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
const updateParam = useCallback(
|
const updateParam = useCallback(
|
||||||
(key: string, value: unknown) => {
|
(key: string, value: unknown) => {
|
||||||
setParams((prev) => {
|
setParams((prev) => {
|
||||||
const next = { ...prev, [key]: value };
|
const next = { ...prev };
|
||||||
|
if (value === undefined) {
|
||||||
|
delete next[key];
|
||||||
|
} else {
|
||||||
|
next[key] = value;
|
||||||
|
}
|
||||||
const id = nodeIdRef.current;
|
const id = nodeIdRef.current;
|
||||||
if (id) {
|
if (id) {
|
||||||
if (notifyParentTimeoutRef.current != null) {
|
if (notifyParentTimeoutRef.current != null) {
|
||||||
|
|
@ -135,6 +245,139 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}, [requiredErrors, nodeType, language]);
|
}, [requiredErrors, nodeType, language]);
|
||||||
|
|
||||||
|
const extractContentAccordionItems = useMemo((): AccordionListItem<string>[] | null => {
|
||||||
|
if (!node || !nodeType || node.type !== CONTEXT_EXTRACT_CONTENT_NODE_TYPE) return null;
|
||||||
|
|
||||||
|
const byName = new Map((nodeType.parameters ?? []).map((p) => [p.name, p]));
|
||||||
|
const out: AccordionListItem<string>[] = [];
|
||||||
|
|
||||||
|
for (const param of sortedParameters) {
|
||||||
|
if (param.frontendType === 'hidden') continue;
|
||||||
|
if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue;
|
||||||
|
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue;
|
||||||
|
|
||||||
|
const usePicker = _shouldUseRequiredPicker(param);
|
||||||
|
if (usePicker) {
|
||||||
|
out.push({
|
||||||
|
id: param.name,
|
||||||
|
title: accordionExtractParamTitle(param, t),
|
||||||
|
children: (
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
{verboseSchemaTypeBadge(verboseSchema, param, t)}
|
||||||
|
<RequiredAttributePicker
|
||||||
|
label={getLabel(param.description, language) || param.name}
|
||||||
|
expectedType={param.type}
|
||||||
|
value={workflowParamUiValue(params, param)}
|
||||||
|
onChange={(val) => updateParam(param.name, val)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontendType = param.frontendType || 'text';
|
||||||
|
const Renderer = FRONTEND_TYPE_RENDERERS[frontendType] ?? FRONTEND_TYPE_RENDERERS.text;
|
||||||
|
|
||||||
|
if (param.name === 'outputMode') {
|
||||||
|
const chunksNested = effectiveSchemaParamString('outputMode', params, nodeType) === 'chunks';
|
||||||
|
out.push({
|
||||||
|
id: param.name,
|
||||||
|
title: accordionExtractParamTitle(param, t),
|
||||||
|
children: (
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
{verboseSchemaTypeBadge(verboseSchema, param, t)}
|
||||||
|
<Renderer
|
||||||
|
param={param}
|
||||||
|
value={workflowParamUiValue(params, param)}
|
||||||
|
onChange={(val: unknown) => updateParam(param.name, val)}
|
||||||
|
allParams={params}
|
||||||
|
instanceId={instanceId}
|
||||||
|
request={request}
|
||||||
|
nodeType={node.type}
|
||||||
|
onPatchParams={patchParams}
|
||||||
|
hideAccordionTitle
|
||||||
|
/>
|
||||||
|
{chunksNested ? (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<AccordionList<string>
|
||||||
|
key={`extract-chunks-${node.id}`}
|
||||||
|
defaultOpenId={null}
|
||||||
|
items={CONTEXT_EXTRACT_CHUNK_PARAM_NAMES.map((chunkName): AccordionListItem<string> => {
|
||||||
|
const cp = byName.get(chunkName);
|
||||||
|
if (!cp) {
|
||||||
|
return { id: chunkName, title: chunkName, children: <></> };
|
||||||
|
}
|
||||||
|
const ft = cp.frontendType || 'text';
|
||||||
|
const ChunkRenderer = FRONTEND_TYPE_RENDERERS[ft] ?? FRONTEND_TYPE_RENDERERS.text;
|
||||||
|
return {
|
||||||
|
id: chunkName,
|
||||||
|
title: accordionExtractParamTitle(cp, t),
|
||||||
|
children: (
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
{verboseSchemaTypeBadge(verboseSchema, cp, t)}
|
||||||
|
<ChunkRenderer
|
||||||
|
param={cp}
|
||||||
|
value={workflowParamUiValue(params, cp)}
|
||||||
|
onChange={(val: unknown) => updateParam(cp.name, val)}
|
||||||
|
allParams={params}
|
||||||
|
instanceId={instanceId}
|
||||||
|
request={request}
|
||||||
|
nodeType={node.type}
|
||||||
|
onPatchParams={patchParams}
|
||||||
|
hideAccordionTitle
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push({
|
||||||
|
id: param.name,
|
||||||
|
title: accordionExtractParamTitle(param, t),
|
||||||
|
children: (
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
{verboseSchemaTypeBadge(verboseSchema, param, t)}
|
||||||
|
<Renderer
|
||||||
|
param={param}
|
||||||
|
value={workflowParamUiValue(params, param)}
|
||||||
|
onChange={(val: unknown) => updateParam(param.name, val)}
|
||||||
|
allParams={params}
|
||||||
|
instanceId={instanceId}
|
||||||
|
request={request}
|
||||||
|
nodeType={node.type}
|
||||||
|
onPatchParams={patchParams}
|
||||||
|
hideAccordionTitle
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}, [
|
||||||
|
sortedParameters,
|
||||||
|
params,
|
||||||
|
nodeType,
|
||||||
|
language,
|
||||||
|
node?.id,
|
||||||
|
node?.type,
|
||||||
|
verboseSchema,
|
||||||
|
instanceId,
|
||||||
|
request,
|
||||||
|
patchParams,
|
||||||
|
updateParam,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!node || !nodeType) return null;
|
if (!node || !nodeType) return null;
|
||||||
|
|
||||||
const isTrigger = node.type.startsWith('trigger.');
|
const isTrigger = node.type.startsWith('trigger.');
|
||||||
|
|
@ -239,10 +482,18 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
<strong>{requiredErrors.map((e) => e.paramLabel).join(', ')}</strong>
|
<strong>{requiredErrors.map((e) => e.paramLabel).join(', ')}</strong>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{parameters.map((param: NodeTypeParameter) => {
|
{extractContentAccordionItems !== null ? (
|
||||||
|
<AccordionList<string>
|
||||||
|
key={`${node.id}-extract-accordion`}
|
||||||
|
defaultOpenId={null}
|
||||||
|
items={extractContentAccordionItems}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
parameters.map((param: NodeTypeParameter) => {
|
||||||
// Safety net: hidden params have no UI footprint at all — no row,
|
// Safety net: hidden params have no UI footprint at all — no row,
|
||||||
// no required-mark, no type-badge. Their value is system-set.
|
// no required-mark, no type-badge. Their value is system-set.
|
||||||
if (param.frontendType === 'hidden') return null;
|
if (param.frontendType === 'hidden') return null;
|
||||||
|
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) return null;
|
||||||
const useRequiredPicker = _shouldUseRequiredPicker(param);
|
const useRequiredPicker = _shouldUseRequiredPicker(param);
|
||||||
if (useRequiredPicker) {
|
if (useRequiredPicker) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -250,7 +501,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
<RequiredAttributePicker
|
<RequiredAttributePicker
|
||||||
label={getLabel(param.description, language) || param.name}
|
label={getLabel(param.description, language) || param.name}
|
||||||
expectedType={param.type}
|
expectedType={param.type}
|
||||||
value={params[param.name] ?? param.default}
|
value={workflowParamUiValue(params, param)}
|
||||||
onChange={(val) => updateParam(param.name, val)}
|
onChange={(val) => updateParam(param.name, val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -301,7 +552,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
</div>
|
</div>
|
||||||
<Renderer
|
<Renderer
|
||||||
param={param}
|
param={param}
|
||||||
value={params[param.name] ?? param.default}
|
value={workflowParamUiValue(params, param)}
|
||||||
onChange={(val: unknown) => updateParam(param.name, val)}
|
onChange={(val: unknown) => updateParam(param.name, val)}
|
||||||
allParams={params}
|
allParams={params}
|
||||||
instanceId={instanceId}
|
instanceId={instanceId}
|
||||||
|
|
@ -311,7 +562,8 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ export interface FieldRendererProps {
|
||||||
nodeType?: string;
|
nodeType?: string;
|
||||||
/** Atomically merge several parameter keys (e.g. cron + schedule). */
|
/** Atomically merge several parameter keys (e.g. cron + schedule). */
|
||||||
onPatchParams?: (patch: Record<string, unknown>) => void;
|
onPatchParams?: (patch: Record<string, unknown>) => void;
|
||||||
|
/** Hide the prominent ``param.name`` line (e.g. Accordion header already shows it). */
|
||||||
|
hideAccordionTitle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldRendererComponent = ComponentType<FieldRendererProps>;
|
export type FieldRendererComponent = ComponentType<FieldRendererProps>;
|
||||||
|
|
@ -135,25 +137,117 @@ function _normalizedSelectOptions(raw: unknown): Array<{ value: string; label: s
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectInput: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
const SelectInput: React.FC<FieldRendererProps> = ({ param, value, onChange, hideAccordionTitle }) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const options = _normalizedSelectOptions(
|
const options = _normalizedSelectOptions(
|
||||||
param.frontendOptions?.options ?? param.options ?? []
|
param.frontendOptions?.options ?? param.options ?? []
|
||||||
);
|
);
|
||||||
|
const allowClear = !param.required;
|
||||||
|
const current = value === undefined || value === null || value === '' ? '' : String(value);
|
||||||
|
const groupId = `select-segment-${param.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
||||||
|
const titleId = `${groupId}-title`;
|
||||||
|
const descId = `${groupId}-desc`;
|
||||||
|
const showNameLine = !hideAccordionTitle;
|
||||||
|
const labelledBy = showNameLine
|
||||||
|
? param.description
|
||||||
|
? `${titleId} ${descId}`
|
||||||
|
: titleId
|
||||||
|
: param.description
|
||||||
|
? descId
|
||||||
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
{showNameLine ? (
|
||||||
<select
|
<div
|
||||||
value={typeof value === 'string' ? value : ''}
|
id={titleId}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
style={{
|
||||||
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc' }}
|
display: 'block',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
marginBottom: param.description ? 4 : 6,
|
||||||
|
color: 'var(--text-primary, #212529)',
|
||||||
|
letterSpacing: '0.01em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{param.name}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{param.description ? (
|
||||||
|
<div
|
||||||
|
id={descId}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 1.35,
|
||||||
|
marginBottom: 6,
|
||||||
|
color: 'var(--text-secondary, #555)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{param.description}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby={labelledBy ?? undefined}
|
||||||
|
aria-label={!labelledBy ? param.name : undefined}
|
||||||
|
aria-required={param.required ? true : undefined}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 6,
|
||||||
|
alignItems: 'stretch',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{options.map((opt) => {
|
||||||
|
const selected = current === opt.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={selected}
|
||||||
|
title={
|
||||||
|
allowClear && selected
|
||||||
|
? t('Erneut klicken, um die Auswahl aufzuheben')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (allowClear && selected) {
|
||||||
|
onChange(undefined);
|
||||||
|
} else {
|
||||||
|
onChange(opt.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minWidth: 'min(100%, 72px)',
|
||||||
|
maxWidth: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '6px 10px',
|
||||||
|
fontSize: 11,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
borderRadius: 6,
|
||||||
|
border: selected
|
||||||
|
? '2px solid var(--primary-color, #0d6efd)'
|
||||||
|
: '1px solid var(--border-color, #ccc)',
|
||||||
|
background: selected
|
||||||
|
? 'var(--primary-soft-bg, rgba(13, 110, 253, 0.12))'
|
||||||
|
: 'var(--panel-subtle-bg, #f8f9fa)',
|
||||||
|
color: selected
|
||||||
|
? 'var(--primary-color, #0a58ca)'
|
||||||
|
: 'var(--text-primary, #212529)',
|
||||||
|
fontWeight: selected ? 600 : 400,
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: selected ? 'inset 0 0 0 1px rgba(13, 110, 253, 0.15)' : 'none',
|
||||||
|
transition: 'background 0.12s ease, border-color 0.12s ease, color 0.12s ease',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="">—</option>
|
|
||||||
{options.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</option>
|
</button>
|
||||||
))}
|
);
|
||||||
</select>
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue