dyn options in api

This commit is contained in:
ValueOn AG 2026-01-22 18:52:07 +01:00
parent f99b6b7604
commit b207c0cc5b

View file

@ -82,6 +82,8 @@ export interface FormGeneratorFormProps<T = any> {
transformField?: (attribute: AttributeDefinition) => AttributeDefinition;
// Optional: Custom validation function
customValidator?: (formData: T, attributes: AttributeDefinition[]) => Record<string, string>;
// Optional: Feature instance ID for feature-scoped options (replaces {instanceId} in API paths)
instanceId?: string;
}
// FormGeneratorForm component - Backend-driven form generation
@ -98,9 +100,23 @@ export function FormGeneratorForm<T extends Record<string, any>>({
attributes: providedAttributes,
filterFields,
transformField,
customValidator
customValidator,
instanceId
}: FormGeneratorFormProps<T>) {
const { t } = useLanguage();
// Helper to resolve API paths with {instanceId} placeholder
const resolveApiPath = (path: string): string => {
if (path.includes('{instanceId}')) {
const resolvedInstanceId = instanceId || (data as any)?.featureInstanceId;
if (!resolvedInstanceId) {
console.warn(`API path "${path}" requires instanceId but none provided`);
return path;
}
return path.replace('{instanceId}', resolvedInstanceId);
}
return path;
};
const [formData, setFormData] = useState<T>(data || {} as T);
const [errors, setErrors] = useState<Record<string, string>>({});
const [fieldFocused, setFieldFocused] = useState<Record<string, boolean>>({});
@ -234,7 +250,8 @@ export function FormGeneratorForm<T extends Record<string, any>>({
setFieldFocused({});
}, [data, getFilteredAttributes]);
// Fetch options for fields with optionsReference
// Fetch options for fields with optionsReference (API path)
// Backend provides options in standardized format: { value, label }
useEffect(() => {
const fetchOptions = async () => {
const filteredAttrs = getFilteredAttributes();
@ -250,39 +267,32 @@ export function FormGeneratorForm<T extends Record<string, any>>({
for (const field of fieldsToFetch) {
if (typeof field.options !== 'string') continue;
const optionKey = field.options;
setLoadingOptions(prev => ({ ...prev, [field.name]: true }));
try {
const response = await api.get(`/api/options/${field.options}`);
// Backend provides full API path (e.g., "/api/connections/statuses/options")
// Resolve {instanceId} placeholder if present
const apiPath = resolveApiPath(optionKey);
const response = await api.get(apiPath);
let fetchedOptions: Array<{ value: string | number; label: string }> = [];
if (Array.isArray(response.data)) {
// Backend returns standardized format: [{ value, label }]
fetchedOptions = response.data.map((opt: any) => {
if (typeof opt === 'string' || typeof opt === 'number') {
return { value: opt, label: String(opt) };
}
// Handle multilingual labels
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
};
});
} else if (response.data?.options && Array.isArray(response.data.options)) {
fetchedOptions = response.data.options.map((opt: any) => {
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
};
return { value: opt.value, label: labelValue };
});
}
setOptionsCache(prev => ({ ...prev, [field.options as string]: fetchedOptions }));
setOptionsCache(prev => ({ ...prev, [optionKey]: fetchedOptions }));
} catch (error: any) {
console.error(`Failed to fetch options for ${field.options}:`, error);
setOptionsCache(prev => ({ ...prev, [field.options as string]: [] }));
@ -293,7 +303,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
};
fetchOptions();
}, [getFilteredAttributes, optionsCache]);
}, [getFilteredAttributes, optionsCache, resolveApiPath]);
// Handle field focus
const handleFieldFocus = (fieldName: string, focused: boolean) => {