From ec752ffd92962f62289dda4e7a787e082e39e6f2 Mon Sep 17 00:00:00 2001 From: fedepini Date: Thu, 14 May 2026 17:44:47 +0200 Subject: [PATCH 01/16] feat: updated Form type to introduce displayingDependencies prop --- src/widgets/Form/Form.schema.json | 92 +++++++++++++++++++++++++ src/widgets/Form/Form.type.d.ts | 64 +++++++++++++++++ src/widgets/Form/fields/AsyncSelect.tsx | 3 +- 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/src/widgets/Form/Form.schema.json b/src/widgets/Form/Form.schema.json index 03364ca..d2c0cd0 100644 --- a/src/widgets/Form/Form.schema.json +++ b/src/widgets/Form/Form.schema.json @@ -422,6 +422,98 @@ } } }, + "displayDependencies": { + "type": "array", + "description": "Configuration for the form fields whose displaying is dependent from other form fields.", + "items": { + "type": "object", + "required": ["dependsOn", "name"], + "additionalProperties": false, + "properties": { + "name": { + "description": "the name of the field", + "type": "string" + }, + "dependsOn": { + "description": "the field on which this field depends on", + "type": "object", + "additionalProperties": false, + "required": ["name", "conditionType"], + "properties": { + "name": { + "type": "string", + "description": "the name of the field on which this field depends on" + }, + "conditionType": { + "type": "string", + "description": "`notEmpty` (default) indicates that the current field is displayed if the dependency field has a non-empty value. `value` indicates that the current field is displayed only when the dependency field matches a specific value.", + "enum": ["notEmpty", "value"] + }, + "value": { + "type": "object", + "description": "Expected value used when `type = value`.", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "decimal", + "boolean", + "array", + "option", + "null" + ], + "description": "Expected dependency value type." + }, + "stringValue": { + "type": "string", + "description": "Value if type = string" + }, + "numberValue": { + "type": "integer", + "description": "Value if type = number or integer" + }, + "decimalValue": { + "type": "string", + "description": "Value if type = number or decimal" + }, + "booleanValue": { + "type": "boolean", + "description": "Value if type = boolean" + }, + "arrayValue": { + "type": "array", + "description": "Value if type = array", + "items": { + "type": "string" + } + }, + "optionValue": { + "description": "Value if type = option", + "additionalProperties": false, + "required": ["value"], + "properties": { + "label": { + "description": "Label of the option", + "type": "string" + }, + "value": { + "description": "Value of the option", + "type": ["string", "number", "boolean"] + } + } + } + } + } + } + } + } + } + }, "objectFields": { "type": "array", "description": "configuration for object fields in the form", diff --git a/src/widgets/Form/Form.type.d.ts b/src/widgets/Form/Form.type.d.ts index 2b91395..c577674 100644 --- a/src/widgets/Form/Form.type.d.ts +++ b/src/widgets/Form/Form.type.d.ts @@ -306,6 +306,70 @@ export interface Form { */ resourceRefId: string }[] + /** + * Configuration for the form fields whose displaying is dependent from other form fields. + */ + displayDependencies?: { + /** + * the name of the field + */ + name: string + /** + * the field on which this field depends on + */ + dependsOn: { + /** + * the name of the field on which this field depends on + */ + name: string + /** + * `notEmpty` (default) indicates that the current field is displayed if the dependency field has a non-empty value. `value` indicates that the current field is displayed only when the dependency field matches a specific value. + */ + conditionType: 'notEmpty' | 'value' + /** + * Expected value used when `type = value`. + */ + value?: { + /** + * Expected dependency value type. + */ + type: 'string' | 'number' | 'integer' | 'decimal' | 'boolean' | 'array' | 'option' | 'null' + /** + * Value if type = string + */ + stringValue?: string + /** + * Value if type = number or integer + */ + numberValue?: number + /** + * Value if type = number or decimal + */ + decimalValue?: string + /** + * Value if type = boolean + */ + booleanValue?: boolean + /** + * Value if type = array + */ + arrayValue?: string[] + /** + * Value if type = option + */ + optionValue?: { + /** + * Label of the option + */ + label?: string + /** + * Value of the option + */ + value: string | number | boolean + } + } + } + }[] /** * configuration for object fields in the form */ diff --git a/src/widgets/Form/fields/AsyncSelect.tsx b/src/widgets/Form/fields/AsyncSelect.tsx index 31fea50..060993c 100644 --- a/src/widgets/Form/fields/AsyncSelect.tsx +++ b/src/widgets/Form/fields/AsyncSelect.tsx @@ -59,8 +59,7 @@ const AsyncSelect = ({ data, form, initialValue, resourcesRefs }: AsyncSelectPro const { data: options = [], isLoading } = useQuery({ enabled: !!(queryValue && config), queryFn: () => getOptionsFromResourceRefId(queryValue as string, resourceRefId, resourcesRefs, key, notification, config), - // eslint-disable-next-line @tanstack/query/exhaustive-deps - queryKey: ['async-select-options', resourceRefId, dependFieldValue, key], + queryKey: ['async-select-options', resourceRefId, dependFieldValue, key, queryValue, resourcesRefs, notification, config], refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000, }) From 314ffe57a3a331731448a7065a3f5997391cd0e9 Mon Sep 17 00:00:00 2001 From: fedepini Date: Fri, 15 May 2026 10:54:26 +0200 Subject: [PATCH 02/16] feat: started updating Form widget --- src/widgets/Form/Form.schema.json | 2 +- src/widgets/Form/Form.tsx | 2 ++ src/widgets/Form/Form.type.d.ts | 2 +- src/widgets/Form/FormGenerator.tsx | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/widgets/Form/Form.schema.json b/src/widgets/Form/Form.schema.json index d2c0cd0..ba573ae 100644 --- a/src/widgets/Form/Form.schema.json +++ b/src/widgets/Form/Form.schema.json @@ -422,7 +422,7 @@ } } }, - "displayDependencies": { + "displayingDependencies": { "type": "array", "description": "Configuration for the form fields whose displaying is dependent from other form fields.", "items": { diff --git a/src/widgets/Form/Form.tsx b/src/widgets/Form/Form.tsx index 8e9ea6c..b5f30ed 100644 --- a/src/widgets/Form/Form.tsx +++ b/src/widgets/Form/Form.tsx @@ -80,6 +80,7 @@ const Form = ({ resourcesRefs, widget, widgetData }: WidgetProps buttonConfig, dependencies, displayMenu, + displayingDependencies, fieldDescription, initialValues, objectFields, @@ -172,6 +173,7 @@ const Form = ({ resourcesRefs, widget, widgetData }: WidgetProps dependencies={dependencies} descriptionTooltip={fieldDescription === 'tooltip'} displayMenu={displayMenu} + displayingDependencies={displayingDependencies} formId={formId} initialValues={initialValues} objectFields={objectFields} diff --git a/src/widgets/Form/Form.type.d.ts b/src/widgets/Form/Form.type.d.ts index c577674..b9550cf 100644 --- a/src/widgets/Form/Form.type.d.ts +++ b/src/widgets/Form/Form.type.d.ts @@ -309,7 +309,7 @@ export interface Form { /** * Configuration for the form fields whose displaying is dependent from other form fields. */ - displayDependencies?: { + displayingDependencies?: { /** * the name of the field */ diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index c9c28fd..ea4ed9f 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -26,6 +26,7 @@ type FormGeneratorType = { schema: JSONSchema4 autocomplete?: FormWidgetData['autocomplete'] dependencies?: FormWidgetData['dependencies'] + displayingDependencies?: FormWidgetData['displayingDependencies'] displayMenu?: FormWidgetData['displayMenu'] objectFields?: FormWidgetData['objectFields'] initialValues?: FormWidgetData['initialValues'] @@ -82,6 +83,7 @@ const FormGenerator = ({ dependencies, descriptionTooltip = false, displayMenu = true, + displayingDependencies, formId, initialValues, objectFields, From c9de203826eb899c91fd50272f2d7e0e05f20c70 Mon Sep 17 00:00:00 2001 From: fedepini Date: Mon, 18 May 2026 17:04:34 +0200 Subject: [PATCH 03/16] feat: updated FormGenerator to include displayingDependencies --- src/widgets/Form/FormGenerator.tsx | 241 +++++++++++++++++++++-------- 1 file changed, 179 insertions(+), 62 deletions(-) diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index ea4ed9f..a9799ae 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -5,7 +5,7 @@ import type { AnchorLinkItemProps } from 'antd/es/anchor/Anchor' import type { Rule } from 'antd/es/form' import type { DefaultOptionType } from 'antd/es/select' import type { JSONSchema4 } from 'json-schema' -import type { ValidateErrorEntity } from 'rc-field-form/lib/interface' +import type { Store, ValidateErrorEntity } from 'rc-field-form/lib/interface' import { useCallback, useEffect, useMemo, useState } from 'react' import ListEditor from '../../components/ListEditor' @@ -63,6 +63,10 @@ const isObjectSchema = (property: JSONSchema4) => { const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null +const isOptionValue = (value: unknown): value is { label?: string; value: string | number | boolean } => { + return (typeof value === 'object' && value !== null && 'value' in value) +} + const getInitialValue = (object: unknown, path: string): unknown => { const pathParts = path.split('.') @@ -78,6 +82,31 @@ const getInitialValue = (object: unknown, path: string): unknown => { return value } +const FormFieldWrapper = ({ + children, + formItemProps, + id, + optionalHidden, + required, +}: { + id: string + required: boolean + optionalHidden: boolean + children: React.ReactNode + formItemProps: React.ComponentProps +}) => { + return ( +
+ +
+ ) +} + const FormGenerator = ({ autocomplete, dependencies, @@ -318,6 +347,14 @@ const FormGenerator = ({ return [] } + const shouldUpdateField = (prev: Store, curr: Store, displayingDependencyPath?: string) => { + if (!displayingDependencyPath) { + return false + } + + return (getInitialValue(prev, displayingDependencyPath) !== getInitialValue(curr, displayingDependencyPath)) + } + const renderField = (label: string, name: string, node: JSONSchema4, required: boolean, formInstance?: FormInstance) => { const currentForm = formInstance ?? form @@ -341,6 +378,78 @@ const FormGenerator = ({ rules.push({ message: 'Insert right value', pattern: new RegExp(node.pattern) }) } + const isVisible = (() => { + const dependency = displayingDependencies?.find(field => field.name === name) + + if (!dependency) { + return true + } + + const dependencyValue = currentForm.getFieldValue(dependency.dependsOn.name.split('.')) as unknown + + if (dependency.dependsOn.conditionType === 'notEmpty') { + if (dependencyValue === undefined || dependencyValue === null) { + return false + } + + if (typeof dependencyValue === 'string') { + return dependencyValue.trim().length > 0 + } + + if (Array.isArray(dependencyValue)) { + return dependencyValue.length > 0 + } + + return true + } + + if (dependency.dependsOn.conditionType === 'value') { + const expected = dependency.dependsOn.value + + if (!expected) { + return true + } + + switch (expected.type) { + case 'string': + return dependencyValue === expected.stringValue + case 'number': + case 'integer': + return dependencyValue === expected.numberValue + case 'decimal': + return String(dependencyValue) === expected.decimalValue + case 'boolean': + return dependencyValue === expected.booleanValue + case 'array': + return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) + case 'option': { + if (!isOptionValue(dependencyValue) || !expected.optionValue) { + return false + } + + const expectedOption = expected.optionValue + const sameValue = dependencyValue.value === expectedOption.value + const sameLabel = expectedOption.label === undefined || dependencyValue.label === expectedOption.label + + return sameValue && sameLabel + } + case 'null': + return dependencyValue === null + default: + return true + } + } + + return true + })() + + if (!isVisible) { + return null + } + + const displayingDependency = displayingDependencies?.find(field => field.name === name) + const displayingDependencyPath = displayingDependency?.dependsOn.name + switch (node.type) { case 'string': { const formItemContent = (() => { @@ -409,42 +518,46 @@ const FormGenerator = ({ })() return ( -
- -
+ shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath), + tooltip: descriptionTooltip && node.description ? node.description : undefined, + }} + id={name} + optionalHidden={optionalHidden} + required={required} + > + {formItemContent} + ) } case 'boolean': return ( -
- - - -
+ + shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + tooltip: descriptionTooltip && node.description ? node.description : undefined, + valuePropName: 'checked', + }} + id={name} + optionalHidden={optionalHidden} + required={required} + > + + + ) case 'array': { @@ -483,19 +596,21 @@ const FormGenerator = ({ })() return ( -
- -
+ shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + tooltip: descriptionTooltip && node.description ? node.description : undefined, + }} + id={name} + optionalHidden={optionalHidden} + required={required} + > + {formItemContent} + ) } @@ -509,23 +624,25 @@ const FormGenerator = ({ } return ( -
- -
+ shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + tooltip: descriptionTooltip && node.description ? node.description : undefined, + }} + id={name} + optionalHidden={optionalHidden} + required={required} + > + {min && max && max - min < 100 ? ( + + ) : ( + + )} + ) } } From 0ef01e02baca90ae730c7d26339eb04e6738a16c Mon Sep 17 00:00:00 2001 From: fedepini Date: Mon, 18 May 2026 18:58:14 +0200 Subject: [PATCH 04/16] feat: updated logic --- src/examples/widgets/Form/Form.example.yaml | 39 +++ src/examples/widgets/Form/Form.menu.yaml | 27 ++ src/widgets/Form/FormGenerator.tsx | 280 +++++++++++--------- 3 files changed, 220 insertions(+), 126 deletions(-) diff --git a/src/examples/widgets/Form/Form.example.yaml b/src/examples/widgets/Form/Form.example.yaml index 0fc9bd6..be2a1ec 100644 --- a/src/examples/widgets/Form/Form.example.yaml +++ b/src/examples/widgets/Form/Form.example.yaml @@ -1260,6 +1260,45 @@ spec: resource: restactions verb: GET --- +# Displaying dependencies field +# Target: validate fields whose displaying depend on the value of another field +# Expected behavior: initially one field is rendered, with the following rendered only if the previous one is compiled +kind: Form +apiVersion: widgets.templates.krateo.io/v1beta1 +metadata: + name: example-form-displaying-dependencies + namespace: krateo-system +spec: + widgetData: + displayingDependencies: + - name: surname + dependsOn: + name: name + conditionType: notEmpty + submitActionId: submit-autocomplete + stringSchema: | + { + "type": "object", + "properties": { + "name": { "type": "string", "title": "Name" }, + "surname": { "type": "string", "title": "Surname" } + } + } + actions: + rest: + - id: submit-autocomplete + type: rest + resourceRefId: resource-ref-1 + headers: [] + resourcesRefs: + items: + - id: resource-ref-1 + apiVersion: composition.krateo.io/v1 + name: submit-autocomplete + namespace: krateo-system + resource: fireworksapps + verb: POST +--- # Missing action # Target: validate error handling when action is missing. # Expected behavior: an error is displayed on submit to inform that the action is not defined. diff --git a/src/examples/widgets/Form/Form.menu.yaml b/src/examples/widgets/Form/Form.menu.yaml index 2c0cac2..8bfe706 100644 --- a/src/examples/widgets/Form/Form.menu.yaml +++ b/src/examples/widgets/Form/Form.menu.yaml @@ -79,6 +79,7 @@ spec: allowedResources: - panels items: + - resourceRefId: example-form-displaying-dependencies-panel - resourceRefId: example-form-missing-schema-panel - resourceRefId: example-form-basic-strings-panel - resourceRefId: example-form-basic-menu-hidden-panel @@ -228,6 +229,12 @@ spec: namespace: krateo-system resource: panels verb: GET + - id: example-form-displaying-dependencies-panel + apiVersion: widgets.templates.krateo.io/v1beta1 + name: example-form-displaying-dependencies-panel + namespace: krateo-system + resource: panels + verb: GET --- kind: Column apiVersion: widgets.templates.krateo.io/v1beta1 @@ -796,6 +803,26 @@ spec: --- kind: Panel apiVersion: widgets.templates.krateo.io/v1beta1 +metadata: + name: example-form-displaying-dependencies-panel + namespace: krateo-system +spec: + widgetData: + actions: {} + title: 'Displaying dependencies field' + items: + - resourceRefId: example-form-displaying-dependencies + resourcesRefs: + items: + - id: example-form-displaying-dependencies + apiVersion: widgets.templates.krateo.io/v1beta1 + name: example-form-displaying-dependencies + namespace: krateo-system + resource: forms + verb: GET +--- +kind: Panel +apiVersion: widgets.templates.krateo.io/v1beta1 metadata: name: example-form-missing-action-panel namespace: krateo-system diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index a9799ae..3146abe 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -107,6 +107,79 @@ const FormFieldWrapper = ({ ) } +const VisibilityGate = ({ + children, + displayingDependencies, + form, + name, +}: { + form: FormInstance + name: string + displayingDependencies?: FormWidgetData['displayingDependencies'] + children: React.ReactNode +}) => { + const dependency = displayingDependencies?.find(field => field.name === name) + + const watchedValue = Form.useWatch( + dependency?.dependsOn.name?.split('.') ?? [], + form, + ) as unknown + + if (!dependency) { return <>{children} } + + const dependencyValue = watchedValue + + const isVisible = (() => { + if (dependency.dependsOn.conditionType === 'notEmpty') { + if (dependencyValue === undefined || dependencyValue === null) { return false } + + if (typeof dependencyValue === 'string') { + return dependencyValue.trim().length > 0 + } + + if (Array.isArray(dependencyValue)) { + return dependencyValue.length > 0 + } + + return true + } + + if (dependency.dependsOn.conditionType === 'value') { + const expected = dependency.dependsOn.value + if (!expected) { return true } + + switch (expected.type) { + case 'string': + return dependencyValue === expected.stringValue + case 'number': + case 'integer': + return dependencyValue === expected.numberValue + case 'decimal': + return String(dependencyValue) === expected.decimalValue + case 'boolean': + return dependencyValue === expected.booleanValue + case 'array': + return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) + case 'option': + return ( + isOptionValue(dependencyValue) && + dependencyValue.value === expected.optionValue?.value + ) + case 'null': + return dependencyValue === null + default: + return true + } + } + + return true + })() + + if (!isVisible) { return null } + + return <>{children} +} + const FormGenerator = ({ autocomplete, dependencies, @@ -378,75 +451,6 @@ const FormGenerator = ({ rules.push({ message: 'Insert right value', pattern: new RegExp(node.pattern) }) } - const isVisible = (() => { - const dependency = displayingDependencies?.find(field => field.name === name) - - if (!dependency) { - return true - } - - const dependencyValue = currentForm.getFieldValue(dependency.dependsOn.name.split('.')) as unknown - - if (dependency.dependsOn.conditionType === 'notEmpty') { - if (dependencyValue === undefined || dependencyValue === null) { - return false - } - - if (typeof dependencyValue === 'string') { - return dependencyValue.trim().length > 0 - } - - if (Array.isArray(dependencyValue)) { - return dependencyValue.length > 0 - } - - return true - } - - if (dependency.dependsOn.conditionType === 'value') { - const expected = dependency.dependsOn.value - - if (!expected) { - return true - } - - switch (expected.type) { - case 'string': - return dependencyValue === expected.stringValue - case 'number': - case 'integer': - return dependencyValue === expected.numberValue - case 'decimal': - return String(dependencyValue) === expected.decimalValue - case 'boolean': - return dependencyValue === expected.booleanValue - case 'array': - return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) - case 'option': { - if (!isOptionValue(dependencyValue) || !expected.optionValue) { - return false - } - - const expectedOption = expected.optionValue - const sameValue = dependencyValue.value === expectedOption.value - const sameLabel = expectedOption.label === undefined || dependencyValue.label === expectedOption.label - - return sameValue && sameLabel - } - case 'null': - return dependencyValue === null - default: - return true - } - } - - return true - })() - - if (!isVisible) { - return null - } - const displayingDependency = displayingDependencies?.find(field => field.name === name) const displayingDependencyPath = displayingDependency?.dependsOn.name @@ -518,45 +522,57 @@ const FormGenerator = ({ })() return ( - shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath), - tooltip: descriptionTooltip && node.description ? node.description : undefined, - }} - id={name} - optionalHidden={optionalHidden} - required={required} + - {formItemContent} - - ) - } - - case 'boolean': - return ( - shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + shouldUpdate: (prev, curr) => shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath), tooltip: descriptionTooltip && node.description ? node.description : undefined, - valuePropName: 'checked', }} id={name} optionalHidden={optionalHidden} required={required} > - + {formItemContent} + + ) + } + + case 'boolean': + return ( + + + shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + tooltip: descriptionTooltip && node.description ? node.description : undefined, + valuePropName: 'checked', + }} + id={name} + optionalHidden={optionalHidden} + required={required} + > + + + ) @@ -596,21 +612,27 @@ const FormGenerator = ({ })() return ( - shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), - tooltip: descriptionTooltip && node.description ? node.description : undefined, - }} - id={name} - optionalHidden={optionalHidden} - required={required} + - {formItemContent} - + shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + tooltip: descriptionTooltip && node.description ? node.description : undefined, + }} + id={name} + optionalHidden={optionalHidden} + required={required} + > + {formItemContent} + + ) } @@ -624,25 +646,31 @@ const FormGenerator = ({ } return ( - shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), - tooltip: descriptionTooltip && node.description ? node.description : undefined, - }} - id={name} - optionalHidden={optionalHidden} - required={required} + - {min && max && max - min < 100 ? ( - - ) : ( - - )} - + shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + tooltip: descriptionTooltip && node.description ? node.description : undefined, + }} + id={name} + optionalHidden={optionalHidden} + required={required} + > + {min && max && max - min < 100 ? ( + + ) : ( + + )} + + ) } } From 2a78ba9ece0e4128a955ca1ff0b21989c763e482 Mon Sep 17 00:00:00 2001 From: fedepini Date: Mon, 18 May 2026 19:14:05 +0200 Subject: [PATCH 05/16] feat: updated anchor list --- src/widgets/Form/FormGenerator.tsx | 176 ++++++++++++++--------------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index 3146abe..4a7fc3d 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -107,75 +107,79 @@ const FormFieldWrapper = ({ ) } -const VisibilityGate = ({ - children, - displayingDependencies, - form, - name, -}: { - form: FormInstance - name: string - displayingDependencies?: FormWidgetData['displayingDependencies'] - children: React.ReactNode -}) => { - const dependency = displayingDependencies?.find(field => field.name === name) - - const watchedValue = Form.useWatch( - dependency?.dependsOn.name?.split('.') ?? [], - form, - ) as unknown +type DisplayDependency = NonNullable[number] - if (!dependency) { return <>{children} } +const evaluateVisibility = (dependency: DisplayDependency | undefined, dependencyValue: unknown): boolean => { + if (!dependency) { + return true + } - const dependencyValue = watchedValue + if (dependency.dependsOn.conditionType === 'notEmpty') { + if (dependencyValue === undefined || dependencyValue === null) { + return false + } - const isVisible = (() => { - if (dependency.dependsOn.conditionType === 'notEmpty') { - if (dependencyValue === undefined || dependencyValue === null) { return false } + if (typeof dependencyValue === 'string') { + return dependencyValue.trim().length > 0 + } - if (typeof dependencyValue === 'string') { - return dependencyValue.trim().length > 0 - } + if (Array.isArray(dependencyValue)) { + return dependencyValue.length > 0 + } - if (Array.isArray(dependencyValue)) { - return dependencyValue.length > 0 - } + return true + } + if (dependency.dependsOn.conditionType === 'value') { + const expected = dependency.dependsOn.value + if (!expected) { return true } - if (dependency.dependsOn.conditionType === 'value') { - const expected = dependency.dependsOn.value - if (!expected) { return true } - - switch (expected.type) { - case 'string': - return dependencyValue === expected.stringValue - case 'number': - case 'integer': - return dependencyValue === expected.numberValue - case 'decimal': - return String(dependencyValue) === expected.decimalValue - case 'boolean': - return dependencyValue === expected.booleanValue - case 'array': - return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) - case 'option': - return ( - isOptionValue(dependencyValue) && - dependencyValue.value === expected.optionValue?.value - ) - case 'null': - return dependencyValue === null - default: - return true - } + switch (expected.type) { + case 'string': + return dependencyValue === expected.stringValue + case 'number': + case 'integer': + return dependencyValue === expected.numberValue + case 'decimal': + return String(dependencyValue) === expected.decimalValue + case 'boolean': + return dependencyValue === expected.booleanValue + case 'array': + return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) + case 'option': + return ( + isOptionValue(dependencyValue) && + dependencyValue.value === expected.optionValue?.value + ) + case 'null': + return dependencyValue === null + default: + return true } + } - return true - })() + return true +} + +const VisibilityGate = ({ + children, + dependency, + form, +}: { + form: FormInstance + dependency?: DisplayDependency + children: React.ReactNode +}) => { + const watchedValue = Form.useWatch( + dependency?.dependsOn.name?.split('.') ?? [], + form, + ) as unknown - if (!isVisible) { return null } + if (!evaluateVisibility(dependency, watchedValue)) { + return null + } return <>{children} } @@ -197,6 +201,12 @@ const FormGenerator = ({ const requiredFields: string[] = Array.isArray(schema.required) ? schema.required : [] const [optionalHidden, setOptionalHidden] = useState(false) + const formValues = Form.useWatch([], form) as Store + const displayingDependenciesMap = useMemo( + () => new Map((displayingDependencies ?? []).map(dep => [dep.name, dep])), + [displayingDependencies], + ) + const [transformedInitialValues, setTransformedInitialValues] = useState(undefined) const { optionalCount, totalCount } = getOptionalCount(schema, requiredFields) @@ -451,7 +461,7 @@ const FormGenerator = ({ rules.push({ message: 'Insert right value', pattern: new RegExp(node.pattern) }) } - const displayingDependency = displayingDependencies?.find(field => field.name === name) + const displayingDependency = displayingDependenciesMap.get(name) const displayingDependencyPath = displayingDependency?.dependsOn.name switch (node.type) { @@ -522,11 +532,7 @@ const FormGenerator = ({ })() return ( - + - + + + { + const getAnchorList = (values: Store): AnchorLinkItemProps[] => { const parseData = ( node: JSONSchema4, name = '', @@ -697,19 +691,25 @@ const FormGenerator = ({ ? parseData(schemaItem, currentName, isRequired) : [] - const shouldShow = isRequired || children.length > 0 || !optionalHidden - if (!shouldShow) { return [] } + const dependency = displayingDependenciesMap.get(currentName) + const dependencyValue = dependency + ? getInitialValue(values, dependency.dependsOn.name) + : undefined + + const isVisible = evaluateVisibility(dependency, dependencyValue) + + const shouldShow = isVisible && (isRequired || children.length > 0 || !optionalHidden) + if (!shouldShow) { + return [] + } return [ { href: schemaItem.type === 'object' ? '#' : `#${currentName}`, key: currentName, - title: - schemaItem.type === 'object' ? ( - {key} - ) : ( - key - ), + title: schemaItem.type === 'object' + ? {key} + : key, ...(children.length > 0 ? { children } : {}), }, ] @@ -796,7 +796,7 @@ const FormGenerator = ({ document.getElementById('anchor-content') as HTMLDivElement} - items={getAnchorList()} + items={getAnchorList(formValues ?? {})} onClick={handleAnchorClick} /> From 740ead37f96a7cf3f69cb7f347d1898333a45fdf Mon Sep 17 00:00:00 2001 From: fedepini Date: Tue, 19 May 2026 18:32:22 +0200 Subject: [PATCH 06/16] feat: updated ListEditor to make it controlled --- src/components/ListEditor/ListEditor.tsx | 43 ++++++++++++--------- src/examples/widgets/Form/Form.example.yaml | 17 +++++++- src/widgets/Form/FormGenerator.tsx | 21 ++++++---- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/components/ListEditor/ListEditor.tsx b/src/components/ListEditor/ListEditor.tsx index 099c544..4161157 100644 --- a/src/components/ListEditor/ListEditor.tsx +++ b/src/components/ListEditor/ListEditor.tsx @@ -1,6 +1,6 @@ import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons' import { Button, Input, List, Space, Typography } from 'antd' -import { useEffect, useState } from 'react' +import { useState } from 'react' type ListEditorType = { data?: string[] @@ -8,39 +8,46 @@ type ListEditorType = { } const ListEditor = ({ data = [], onChange }: ListEditorType) => { - const [currentString, setCurrentString] = useState() - const [list, setList] = useState(data) - - useEffect(() => { - setList(data) - }, [data]) + const [currentString, setCurrentString] = useState('') const onAdd = () => { - if (currentString && currentString !== '') { - const newList = [...list, currentString] - onChange(newList) - setList(newList) + const trimmed = currentString.trim() + + if (trimmed !== '') { + onChange([...data, trimmed]) + setCurrentString('') } } const onRemove = (index: number) => { - const newList = list.filter((_, i) => i !== index) - onChange(newList) - setList(newList) + onChange(data.filter((_, i) => i !== index)) } return ( - setCurrentString(value.target.value)} /> - + ( - } onClick={() => onRemove(index)} shape='circle' type='text' />]}> + } + onClick={() => onRemove(index)} + shape='circle' + type='text' + />, + ]} + > {item} )} diff --git a/src/examples/widgets/Form/Form.example.yaml b/src/examples/widgets/Form/Form.example.yaml index be2a1ec..f9b7139 100644 --- a/src/examples/widgets/Form/Form.example.yaml +++ b/src/examples/widgets/Form/Form.example.yaml @@ -1275,13 +1275,28 @@ spec: dependsOn: name: name conditionType: notEmpty + - name: middleName + dependsOn: + name: name + conditionType: notEmpty + - name: tagDescription + dependsOn: + name: tags + conditionType: notEmpty submitActionId: submit-autocomplete stringSchema: | { "type": "object", "properties": { "name": { "type": "string", "title": "Name" }, - "surname": { "type": "string", "title": "Surname" } + "middleName": { "type": "string", "title": "Middle Name" }, + "surname": { "type": "string", "title": "Surname" }, + "tags": { + "type": "array", + "title": "Tags", + "items": { "type": "string" } + }, + "tagDescription": { "type": "string", "title": "Tag Description" } } } actions: diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index 4a7fc3d..86114ea 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -200,8 +200,8 @@ const FormGenerator = ({ const [form] = Form.useForm() const requiredFields: string[] = Array.isArray(schema.required) ? schema.required : [] const [optionalHidden, setOptionalHidden] = useState(false) + const [anchorValues, setAnchorValues] = useState({}) - const formValues = Form.useWatch([], form) as Store const displayingDependenciesMap = useMemo( () => new Map((displayingDependencies ?? []).map(dep => [dep.name, dep])), [displayingDependencies], @@ -605,9 +605,9 @@ const FormGenerator = ({ return ( { - form.setFieldValue(name.split('.'), values) + currentForm.setFieldValue(name.split('.'), values) }} /> ) @@ -670,7 +670,7 @@ const FormGenerator = ({ } } - const getAnchorList = (values: Store): AnchorLinkItemProps[] => { + const getAnchorList = useCallback((values: Store): AnchorLinkItemProps[] => { const parseData = ( node: JSONSchema4, name = '', @@ -705,9 +705,9 @@ const FormGenerator = ({ return [ { - href: schemaItem.type === 'object' ? '#' : `#${currentName}`, + href: isObjectSchema(schemaItem) ? '#' : `#${currentName}`, key: currentName, - title: schemaItem.type === 'object' + title: isObjectSchema(schemaItem) ? {key} : key, ...(children.length > 0 ? { children } : {}), @@ -717,7 +717,9 @@ const FormGenerator = ({ } return parseData(schema) - } + }, [displayingDependenciesMap, optionalHidden, schema]) + + const anchorItems = useMemo(() => getAnchorList(anchorValues), [anchorValues, getAnchorList]) const onFinishFailed = useCallback(({ errorFields }: ValidateErrorEntity) => { const errorField = errorFields[0].name.join('.') @@ -785,6 +787,9 @@ const FormGenerator = ({ event.preventDefault() setInitialValues() }} + onValuesChange={(_, allValues) => { + setAnchorValues(allValues as Store) + }} > {parseData(schema)} @@ -796,7 +801,7 @@ const FormGenerator = ({ document.getElementById('anchor-content') as HTMLDivElement} - items={getAnchorList(formValues ?? {})} + items={anchorItems} onClick={handleAnchorClick} /> From 31ff6bcc60780f463d1aaeb3c6bb98a29d95c348 Mon Sep 17 00:00:00 2001 From: fedepini Date: Tue, 19 May 2026 21:15:18 +0200 Subject: [PATCH 07/16] feat: updated option logic --- src/examples/widgets/Form/Form.example.yaml | 95 ++++++++++++++++++++- src/widgets/Form/Form.schema.json | 12 +-- src/widgets/Form/Form.type.d.ts | 10 +-- src/widgets/Form/FormGenerator.tsx | 28 ++++-- 4 files changed, 116 insertions(+), 29 deletions(-) diff --git a/src/examples/widgets/Form/Form.example.yaml b/src/examples/widgets/Form/Form.example.yaml index f9b7139..4ca2c4f 100644 --- a/src/examples/widgets/Form/Form.example.yaml +++ b/src/examples/widgets/Form/Form.example.yaml @@ -1283,20 +1283,107 @@ spec: dependsOn: name: tags conditionType: notEmpty + - name: stringTest + dependsOn: + name: typeStringTest + conditionType: value + value: + type: string + stringValue: test + - name: integerTest + dependsOn: + name: typeintegerTest + conditionType: value + value: + type: integer + integerValue: 2 + - name: booleanTest + dependsOn: + name: typeBooleanTest + conditionType: value + value: + type: boolean + booleanValue: true + - name: arrayTest + dependsOn: + name: typeArrayTest + conditionType: value + value: + type: array + arrayValue: ['test'] + - name: optionRadioTest + dependsOn: + name: typeOptionRadioTest + conditionType: value + value: + type: option + optionValue: + value: "1" + - name: optionSelectTest + dependsOn: + name: typeOptionSelectTest + conditionType: value + value: + type: option + optionValue: + value: "1" submitActionId: submit-autocomplete stringSchema: | { "type": "object", "properties": { - "name": { "type": "string", "title": "Name" }, + "name": { + "type": "string", + "title": "Name (tests not empty string)", + "description": "Type something to see two dependent fields displayed ('Middle Name' and 'Surname')" + }, "middleName": { "type": "string", "title": "Middle Name" }, "surname": { "type": "string", "title": "Surname" }, "tags": { "type": "array", - "title": "Tags", - "items": { "type": "string" } + "title": "Tags (tests not empty array)", + "items": { "type": "string" }, + "description": "Add a value to see a dependent field displayed ('Tag Description')" + }, + "tagDescription": { "type": "string", "title": "Tag Description" }, + "typeStringTest": { + "type": "string", + "title": "String test (tests equality on string value)", + "description": "Type 'test' to see a dependent fields displayed ('String test')" + }, + "stringTest": { "type": "string", "title": "String test" }, + "typeIntegerTest": { + "type": "integer", + "title": "Integer test (tests equality on integer value)", + "description": "Type '2' to see a dependent fields displayed ('Integer test')" + }, + "integerTest": { "type": "integer", "title": "Integer test" }, + "typeBooleanTest": { + "type": "boolean", + "title": "Boolean test (tests equality on boolean value)", + "description": "Set switch to true to see a dependent field displayed ('Boolean test'" + }, + "booleanTest": { "type": "boolean", "title": "Boolean test" }, + "typeArrayTest": { + "type": "array", + "title": "Array test (tests equality on array value)", + "description": "Add a 'test' value to see a dependent field displayed ('Array test')" + }, + "arrayTest": { "type": "array", "title": "Array test" }, + "typeOptionRadioTest": { + "type": "string", + "title": "Option test (tests equality on option value - radio button)", + "enum": ["1","2","3"], + "description": "Select the '1' value to see a dependent field displayed ('Option radio test')" + }, + "optionRadioTest": { "type": "string", "title": "Option radio test" }, + "typeOptionSelectTest": { + "type": "string", + "title": "Option test (tests equality on option value - select)", + "enum": ["1","2","3","4","5"], + "description": "Select the '1' value to see a dependent field displayed ('Option select test')" }, - "tagDescription": { "type": "string", "title": "Tag Description" } + "optionSelectTest": { "type": "string", "title": "Option select test" } } } actions: diff --git a/src/widgets/Form/Form.schema.json b/src/widgets/Form/Form.schema.json index ba573ae..6c4377d 100644 --- a/src/widgets/Form/Form.schema.json +++ b/src/widgets/Form/Form.schema.json @@ -459,9 +459,7 @@ "type": "string", "enum": [ "string", - "number", "integer", - "decimal", "boolean", "array", "option", @@ -473,13 +471,9 @@ "type": "string", "description": "Value if type = string" }, - "numberValue": { + "integerValue": { "type": "integer", - "description": "Value if type = number or integer" - }, - "decimalValue": { - "type": "string", - "description": "Value if type = number or decimal" + "description": "Value if type = integer" }, "booleanValue": { "type": "boolean", @@ -493,7 +487,7 @@ } }, "optionValue": { - "description": "Value if type = option", + "description": "Value if type = option: includes a single value or a { label, value } object", "additionalProperties": false, "required": ["value"], "properties": { diff --git a/src/widgets/Form/Form.type.d.ts b/src/widgets/Form/Form.type.d.ts index b9550cf..6b0ad93 100644 --- a/src/widgets/Form/Form.type.d.ts +++ b/src/widgets/Form/Form.type.d.ts @@ -333,19 +333,15 @@ export interface Form { /** * Expected dependency value type. */ - type: 'string' | 'number' | 'integer' | 'decimal' | 'boolean' | 'array' | 'option' | 'null' + type: 'string' | 'integer' | 'boolean' | 'array' | 'option' | 'null' /** * Value if type = string */ stringValue?: string /** - * Value if type = number or integer + * Value if type = integer */ - numberValue?: number - /** - * Value if type = number or decimal - */ - decimalValue?: string + integerValue?: number /** * Value if type = boolean */ diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index 86114ea..d466ccb 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -139,20 +139,30 @@ const evaluateVisibility = (dependency: DisplayDependency | undefined, dependenc switch (expected.type) { case 'string': return dependencyValue === expected.stringValue - case 'number': case 'integer': - return dependencyValue === expected.numberValue - case 'decimal': - return String(dependencyValue) === expected.decimalValue + return dependencyValue === expected.integerValue case 'boolean': return dependencyValue === expected.booleanValue case 'array': return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) - case 'option': - return ( - isOptionValue(dependencyValue) && - dependencyValue.value === expected.optionValue?.value - ) + case 'option': { + const expectedValue = expected.optionValue?.value + + // { label, value } + if (isOptionValue(dependencyValue)) { + return String(dependencyValue.value) === String(expectedValue) + } + + // ['a', 'b'] + if (Array.isArray(dependencyValue)) { + return dependencyValue.some( + value => String(value) === String(expectedValue) + ) + } + + // 'a' | 1 | true + return String(dependencyValue) === String(expectedValue) + } case 'null': return dependencyValue === null default: From d10afec8523a4e39ef3e96967cdc80339c3e3e56 Mon Sep 17 00:00:00 2001 From: fedepini Date: Wed, 20 May 2026 15:41:25 +0200 Subject: [PATCH 08/16] feat: updated multi select option logic --- src/examples/widgets/Form/Form.example.yaml | 15 +++++++++------ src/widgets/Form/FormGenerator.tsx | 7 +++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/examples/widgets/Form/Form.example.yaml b/src/examples/widgets/Form/Form.example.yaml index 4ca2c4f..97ba432 100644 --- a/src/examples/widgets/Form/Form.example.yaml +++ b/src/examples/widgets/Form/Form.example.yaml @@ -1326,7 +1326,7 @@ spec: value: type: option optionValue: - value: "1" + value: ["1", "2"] submitActionId: submit-autocomplete stringSchema: | { @@ -1378,12 +1378,15 @@ spec: }, "optionRadioTest": { "type": "string", "title": "Option radio test" }, "typeOptionSelectTest": { - "type": "string", - "title": "Option test (tests equality on option value - select)", - "enum": ["1","2","3","4","5"], - "description": "Select the '1' value to see a dependent field displayed ('Option select test')" + "type": "array", + "title": "Option test (tests equality on option value - multi select)", + "description": "Select the '1' and '2' value to see a dependent field displayed ('Option multi select test')", + "items": { + "type": "string", + "enum": ["1","2","3"] + } }, - "optionSelectTest": { "type": "string", "title": "Option select test" } + "optionSelectTest": { "type": "string", "title": "Option multi select test" } } } actions: diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index d466ccb..5edf9e4 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -155,8 +155,11 @@ const evaluateVisibility = (dependency: DisplayDependency | undefined, dependenc // ['a', 'b'] if (Array.isArray(dependencyValue)) { - return dependencyValue.some( - value => String(value) === String(expectedValue) + const expectedValues = Array.isArray(expectedValue) ? expectedValue : [expectedValue] + + return ( + dependencyValue.length === expectedValues.length + && expectedValues.every(expectedItem => dependencyValue.includes(String(expectedItem))) ) } From 60605e11a6967448a82d927f021cfb5eb821fdd6 Mon Sep 17 00:00:00 2001 From: fedepini Date: Wed, 20 May 2026 16:40:46 +0200 Subject: [PATCH 09/16] fix: fixed Autocomplete --- src/widgets/Form/fields/AutoComplete.tsx | 8 +++++--- src/widgets/Form/fields/utils.ts | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/widgets/Form/fields/AutoComplete.tsx b/src/widgets/Form/fields/AutoComplete.tsx index 5fe3847..e11872b 100644 --- a/src/widgets/Form/fields/AutoComplete.tsx +++ b/src/widgets/Form/fields/AutoComplete.tsx @@ -7,6 +7,7 @@ import type { DefaultOptionType } from 'antd/es/select' import debounce from 'lodash/debounce' import { useEffect, useMemo, useRef, useState } from 'react' +import { useAuth } from '../../../context/AuthContext' import { useConfigContext } from '../../../context/ConfigContext' import type { ResourcesRefs } from '../../../types/Widget' import type { FormWidgetData } from '../Form' @@ -26,6 +27,8 @@ const AutoComplete = ({ data, form, initialValue, options, resourcesRefs }: Auto const { config } = useConfigContext() const { extra, name, resourceRefId } = data + const { accessToken } = useAuth() + const [searchValue, setSearchValue] = useState('') const [debouncedValue, setDebouncedValue] = useState('') const [inputValue, setInputValue] = useState('') @@ -47,9 +50,8 @@ const AutoComplete = ({ data, form, initialValue, options, resourcesRefs }: Auto const { data: queriedOptions = [], isLoading } = useQuery({ enabled: !!(queryValue && resourceRefId && config), queryFn: () => - getOptionsFromResourceRefId(queryValue as string, resourceRefId, resourcesRefs, extra?.key, notification, config), - // eslint-disable-next-line @tanstack/query/exhaustive-deps - queryKey: ['autocomplete-options', resourceRefId, queryValue, extra?.key], + getOptionsFromResourceRefId(queryValue as string, resourceRefId, resourcesRefs, extra?.key, notification, config, accessToken), + queryKey: ['autocomplete-options', resourceRefId, queryValue, extra?.key, resourcesRefs, notification, config, accessToken], refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000, }) diff --git a/src/widgets/Form/fields/utils.ts b/src/widgets/Form/fields/utils.ts index e8945a1..48f6aec 100644 --- a/src/widgets/Form/fields/utils.ts +++ b/src/widgets/Form/fields/utils.ts @@ -1,7 +1,6 @@ import type { NotificationInstance } from 'antd/es/notification/interface' import type { DefaultOptionType } from 'antd/es/select' -import { useAuth } from '../../../context/AuthContext' import type { Config } from '../../../context/ConfigContext' import type { ResourcesRefs } from '../../../types/Widget' import { getResourceRef } from '../../../utils/utils' @@ -12,10 +11,9 @@ export const getOptionsFromResourceRefId = async ( resourcesRefs: ResourcesRefs, valueKey: string | undefined, notification: NotificationInstance, - config: Config | undefined + config: Config | undefined, + accessToken: string | null ): Promise => { - const { accessToken } = useAuth() - if (!resourceRefId) { return [] } From c61c420760350f6c9448aaf4648f2fa4fe4bba38 Mon Sep 17 00:00:00 2001 From: fedepini Date: Wed, 20 May 2026 16:45:12 +0200 Subject: [PATCH 10/16] feat: added autocomplete example --- src/examples/widgets/Form/Form.example.yaml | 42 ++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/examples/widgets/Form/Form.example.yaml b/src/examples/widgets/Form/Form.example.yaml index 97ba432..5cf005c 100644 --- a/src/examples/widgets/Form/Form.example.yaml +++ b/src/examples/widgets/Form/Form.example.yaml @@ -1270,6 +1270,10 @@ metadata: namespace: krateo-system spec: widgetData: + autocomplete: + - name: typeOptionAutocompleteTest + - name: regions + resourceRefId: get-italian-regions displayingDependencies: - name: surname dependsOn: @@ -1327,6 +1331,23 @@ spec: type: option optionValue: value: ["1", "2"] + - name: optionAutocompleteTest + dependsOn: + name: typeOptionAutocompleteTest + conditionType: value + value: + type: option + optionValue: + value: "1" + - name: optionAutocompleteLabelTest + dependsOn: + name: regions + conditionType: value + value: + type: option + optionValue: + value: "7" + label: "Liguria" submitActionId: submit-autocomplete stringSchema: | { @@ -1386,7 +1407,20 @@ spec: "enum": ["1","2","3"] } }, - "optionSelectTest": { "type": "string", "title": "Option multi select test" } + "optionSelectTest": { "type": "string", "title": "Option multi select test" }, + "typeOptionAutocompleteTest": { + "type": "string", + "title": "Option test (tests equality on option value - autocomplete)", + "description": "Select the '1' value to see a dependent field displayed ('Autocomplete test')", + "enum": ["1","2","3"] + }, + "optionAutocompleteTest": { "type": "string", "title": "Autocomplete test" }, + "regions": { + "type": "string", + "title": "Region (tests equality on option value and label - autocomplete)", + "description": "Select the 'Liguria' value to see a dependent field displayed ('Autocomplete with label test')" + }, + "optionAutocompleteLabelTest": { "type": "string", "title": "Autocomplete with label test" } } } actions: @@ -1403,6 +1437,12 @@ spec: namespace: krateo-system resource: fireworksapps verb: POST + - id: get-italian-regions + apiVersion: templates.krateo.io/v1 + name: get-italian-regions + namespace: krateo-system + resource: restactions + verb: GET --- # Missing action # Target: validate error handling when action is missing. From eef9f84bf5b465c3160affd70a3d73f7c04e1a3a Mon Sep 17 00:00:00 2001 From: fedepini Date: Wed, 20 May 2026 16:51:51 +0200 Subject: [PATCH 11/16] feat: updated integer example --- src/examples/widgets/Form/Form.example.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/widgets/Form/Form.example.yaml b/src/examples/widgets/Form/Form.example.yaml index 5cf005c..2b4000b 100644 --- a/src/examples/widgets/Form/Form.example.yaml +++ b/src/examples/widgets/Form/Form.example.yaml @@ -1296,7 +1296,7 @@ spec: stringValue: test - name: integerTest dependsOn: - name: typeintegerTest + name: typeIntegerTest conditionType: value value: type: integer From 526dea39926383fab509c5aff9be83fe7f62ea70 Mon Sep 17 00:00:00 2001 From: fedepini Date: Fri, 22 May 2026 15:34:16 +0200 Subject: [PATCH 12/16] refactor: started refactoring FormGenerator --- src/widgets/Form/FormFieldWrapper.tsx | 32 ++++++ src/widgets/Form/FormGenerator.tsx | 141 ++------------------------ src/widgets/Form/VisibilityGate.tsx | 32 ++++++ src/widgets/Form/utils.ts | 81 +++++++++++++++ 4 files changed, 152 insertions(+), 134 deletions(-) create mode 100644 src/widgets/Form/FormFieldWrapper.tsx create mode 100644 src/widgets/Form/VisibilityGate.tsx diff --git a/src/widgets/Form/FormFieldWrapper.tsx b/src/widgets/Form/FormFieldWrapper.tsx new file mode 100644 index 0000000..51b91c3 --- /dev/null +++ b/src/widgets/Form/FormFieldWrapper.tsx @@ -0,0 +1,32 @@ +import type { FormItemProps } from 'antd' +import { Form } from 'antd' + +import styles from './Form.module.css' + +type FormFieldWrapperProps = { + children: React.ReactNode + formItemProps: FormItemProps + id: string + optionalHidden: boolean + required: boolean +} + +const FormFieldWrapper = ({ + children, + formItemProps, + id, + optionalHidden, + required, +}: FormFieldWrapperProps) => { + const hidden = optionalHidden && !required + + return ( +
+ +
+ ) +} + +export default FormFieldWrapper diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index 5edf9e4..2491cc5 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -16,7 +16,9 @@ import AsyncSelect from './fields/AsyncSelect' import AutoComplete from './fields/AutoComplete' import type { FormWidgetData } from './Form' import styles from './Form.module.css' -import { getOptionsFromEnum } from './utils' +import FormFieldWrapper from './FormFieldWrapper' +import { evaluateVisibility, getOptionsFromEnum, isObjectSchema, isRecord } from './utils' +import VisibilityGate from './VisibilityGate' type FormGeneratorType = { descriptionTooltip: boolean @@ -32,6 +34,8 @@ type FormGeneratorType = { initialValues?: FormWidgetData['initialValues'] } +export type DisplayDependency = NonNullable[number] + const getOptionalCount = (node: JSONSchema4, requiredFields: string[]) => { const currentProperties = node.properties let totalCount = 0 @@ -55,18 +59,6 @@ const getOptionalCount = (node: JSONSchema4, requiredFields: string[]) => { return { optionalCount, totalCount } } -const isObjectSchema = (property: JSONSchema4) => { - return property.type === 'object' || - (Array.isArray(property.type) && property.type.includes('object')) || - (!!property.properties && typeof property.properties === 'object') -} - -const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null - -const isOptionValue = (value: unknown): value is { label?: string; value: string | number | boolean } => { - return (typeof value === 'object' && value !== null && 'value' in value) -} - const getInitialValue = (object: unknown, path: string): unknown => { const pathParts = path.split('.') @@ -82,121 +74,6 @@ const getInitialValue = (object: unknown, path: string): unknown => { return value } -const FormFieldWrapper = ({ - children, - formItemProps, - id, - optionalHidden, - required, -}: { - id: string - required: boolean - optionalHidden: boolean - children: React.ReactNode - formItemProps: React.ComponentProps -}) => { - return ( -
- -
- ) -} - -type DisplayDependency = NonNullable[number] - -const evaluateVisibility = (dependency: DisplayDependency | undefined, dependencyValue: unknown): boolean => { - if (!dependency) { - return true - } - - if (dependency.dependsOn.conditionType === 'notEmpty') { - if (dependencyValue === undefined || dependencyValue === null) { - return false - } - - if (typeof dependencyValue === 'string') { - return dependencyValue.trim().length > 0 - } - - if (Array.isArray(dependencyValue)) { - return dependencyValue.length > 0 - } - - return true - } - - if (dependency.dependsOn.conditionType === 'value') { - const expected = dependency.dependsOn.value - if (!expected) { - return true - } - - switch (expected.type) { - case 'string': - return dependencyValue === expected.stringValue - case 'integer': - return dependencyValue === expected.integerValue - case 'boolean': - return dependencyValue === expected.booleanValue - case 'array': - return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) - case 'option': { - const expectedValue = expected.optionValue?.value - - // { label, value } - if (isOptionValue(dependencyValue)) { - return String(dependencyValue.value) === String(expectedValue) - } - - // ['a', 'b'] - if (Array.isArray(dependencyValue)) { - const expectedValues = Array.isArray(expectedValue) ? expectedValue : [expectedValue] - - return ( - dependencyValue.length === expectedValues.length - && expectedValues.every(expectedItem => dependencyValue.includes(String(expectedItem))) - ) - } - - // 'a' | 1 | true - return String(dependencyValue) === String(expectedValue) - } - case 'null': - return dependencyValue === null - default: - return true - } - } - - return true -} - -const VisibilityGate = ({ - children, - dependency, - form, -}: { - form: FormInstance - dependency?: DisplayDependency - children: React.ReactNode -}) => { - const watchedValue = Form.useWatch( - dependency?.dependsOn.name?.split('.') ?? [], - form, - ) as unknown - - if (!evaluateVisibility(dependency, watchedValue)) { - return null - } - - return <>{children} -} - const FormGenerator = ({ autocomplete, dependencies, @@ -651,20 +528,16 @@ const FormGenerator = ({ const min = node.minimum const max = node.maximum - const existing = form.getFieldValue(name.split('.')) as number | undefined - if (existing === undefined && min !== undefined) { - form.setFieldValue(name.split('.'), min) - } - return ( shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), + shouldUpdate: (prev, curr) => shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath), tooltip: descriptionTooltip && node.description ? node.description : undefined, }} id={name} diff --git a/src/widgets/Form/VisibilityGate.tsx b/src/widgets/Form/VisibilityGate.tsx new file mode 100644 index 0000000..b308e3d --- /dev/null +++ b/src/widgets/Form/VisibilityGate.tsx @@ -0,0 +1,32 @@ +import { Form, type FormInstance } from 'antd' +import { useMemo } from 'react' + +import type { DisplayDependency } from './FormGenerator' +import { evaluateVisibility } from './utils' + +type VisibilityGateProps = { + children: React.ReactNode + dependency?: DisplayDependency + form: FormInstance +} + +const VisibilityGate = ({ + children, + dependency, + form, +}: VisibilityGateProps) => { + const watchedValue = Form.useWatch( + dependency?.dependsOn.name?.split('.') ?? [], + form, + ) as unknown + + const isVisible = useMemo(() => evaluateVisibility(dependency, watchedValue), [dependency, watchedValue]) + + if (!isVisible) { + return null + } + + return <>{children} +} + +export default VisibilityGate diff --git a/src/widgets/Form/utils.ts b/src/widgets/Form/utils.ts index d098574..be84785 100644 --- a/src/widgets/Form/utils.ts +++ b/src/widgets/Form/utils.ts @@ -1,6 +1,79 @@ import type { DefaultOptionType } from 'antd/es/select' import type { JSONSchema4, JSONSchema4Type } from 'json-schema' +import type { DisplayDependency } from './FormGenerator' + +const isOptionValue = (value: unknown): value is { label?: string; value: string | number | boolean } => { + return (typeof value === 'object' && value !== null && 'value' in value) +} + +export const evaluateVisibility = (dependency: DisplayDependency | undefined, dependencyValue: unknown): boolean => { + if (!dependency) { + return true + } + + if (dependency.dependsOn.conditionType === 'notEmpty') { + if (dependencyValue === undefined || dependencyValue === null) { + return false + } + + if (typeof dependencyValue === 'string') { + return dependencyValue.trim().length > 0 + } + + if (Array.isArray(dependencyValue)) { + return dependencyValue.length > 0 + } + + return true + } + + if (dependency.dependsOn.conditionType === 'value') { + const expected = dependency.dependsOn.value + if (!expected) { + return true + } + + switch (expected.type) { + case 'string': + return dependencyValue === expected.stringValue + case 'integer': + return dependencyValue === expected.integerValue + case 'boolean': + return dependencyValue === expected.booleanValue + case 'array': + return JSON.stringify(dependencyValue) === JSON.stringify(expected.arrayValue) + case 'option': { + const expectedValue = expected.optionValue?.value + + // { label, value } + if (isOptionValue(dependencyValue)) { + return String(dependencyValue.value) === String(expectedValue) + } + + // ['a', 'b'] + if (Array.isArray(dependencyValue)) { + const expectedValues = Array.isArray(expectedValue) ? expectedValue : [expectedValue] + + return ( + dependencyValue.length === expectedValues.length + && expectedValues.every(expectedItem => dependencyValue.includes(String(expectedItem))) + ) + } + + // 'a' | 1 | true + return String(dependencyValue) === String(expectedValue) + } + case 'null': + return dependencyValue === null + default: + return true + } + } + + return true +} + export const getDefaultsFromSchema = (schema: JSONSchema4): Record => { const defaults: Record = {} @@ -25,3 +98,11 @@ export const getOptionsFromEnum = (enumValues: JSONSchema4Type[] | undefined): D .filter((value): value is string | number => typeof value === 'string' || typeof value === 'number') .map((value) => ({ label: String(value), value })) } + +export const isObjectSchema = (property: JSONSchema4) => { + return property.type === 'object' + || (Array.isArray(property.type) && property.type.includes('object')) + || (!!property.properties && typeof property.properties === 'object') +} + +export const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null From 4c8fe7214006e9b5e9bbdd7af4cece91c27caed5 Mon Sep 17 00:00:00 2001 From: fedepini Date: Fri, 22 May 2026 16:09:28 +0200 Subject: [PATCH 13/16] refactor: created FieldContainer --- src/hooks/useFieldVisibility.tsx | 13 +++ src/widgets/Form/FieldContainer.tsx | 72 +++++++++++++ src/widgets/Form/FormFieldWrapper.tsx | 32 ------ src/widgets/Form/FormGenerator.tsx | 147 ++++++++++++-------------- src/widgets/Form/VisibilityGate.tsx | 32 ------ 5 files changed, 154 insertions(+), 142 deletions(-) create mode 100644 src/hooks/useFieldVisibility.tsx create mode 100644 src/widgets/Form/FieldContainer.tsx delete mode 100644 src/widgets/Form/FormFieldWrapper.tsx delete mode 100644 src/widgets/Form/VisibilityGate.tsx diff --git a/src/hooks/useFieldVisibility.tsx b/src/hooks/useFieldVisibility.tsx new file mode 100644 index 0000000..360a99e --- /dev/null +++ b/src/hooks/useFieldVisibility.tsx @@ -0,0 +1,13 @@ +import { Form, type FormInstance } from 'antd' + +import type { DisplayDependency } from '../widgets/Form/FormGenerator' +import { evaluateVisibility } from '../widgets/Form/utils' + +export const useFieldVisibility = (dependency: DisplayDependency | undefined, form: FormInstance) => { + const watchedValue = Form.useWatch( + dependency?.dependsOn.name?.split('.') ?? [], + form, + ) as unknown + + return evaluateVisibility(dependency, watchedValue) +} diff --git a/src/widgets/Form/FieldContainer.tsx b/src/widgets/Form/FieldContainer.tsx new file mode 100644 index 0000000..f64a945 --- /dev/null +++ b/src/widgets/Form/FieldContainer.tsx @@ -0,0 +1,72 @@ +import { Form, type FormInstance } from 'antd' +import type { FormItemProps, Rule } from 'antd/es/form' + +import { useFieldVisibility } from '../../hooks/useFieldVisibility' + +import styles from './Form.module.css' +import type { DisplayDependency } from './FormGenerator' + +type FieldContainerProps = { + children: React.ReactNode + displayingDependency?: DisplayDependency + description?: string + descriptionTooltip: boolean + form: FormInstance + id: string + label: React.ReactNode + name: string[] + optionalHidden: boolean + preserve?: boolean + required: boolean + initialValue?: FormItemProps['initialValue'] + rules?: Rule[] + shouldUpdate?: FormItemProps['shouldUpdate'] + valuePropName?: FormItemProps['valuePropName'] +} + +const FieldContainer = ({ + children, + description, + descriptionTooltip, + displayingDependency, + form, + id, + initialValue, + label, + name, + optionalHidden, + preserve, + required, + rules, + shouldUpdate, + valuePropName, +}: FieldContainerProps) => { + const visible = useFieldVisibility(displayingDependency, form) + + if (!visible) { + return null + } + + const hidden = optionalHidden && !required + + return ( +
+ +
+ ) +} + +export default FieldContainer diff --git a/src/widgets/Form/FormFieldWrapper.tsx b/src/widgets/Form/FormFieldWrapper.tsx deleted file mode 100644 index 51b91c3..0000000 --- a/src/widgets/Form/FormFieldWrapper.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { FormItemProps } from 'antd' -import { Form } from 'antd' - -import styles from './Form.module.css' - -type FormFieldWrapperProps = { - children: React.ReactNode - formItemProps: FormItemProps - id: string - optionalHidden: boolean - required: boolean -} - -const FormFieldWrapper = ({ - children, - formItemProps, - id, - optionalHidden, - required, -}: FormFieldWrapperProps) => { - const hidden = optionalHidden && !required - - return ( -
- -
- ) -} - -export default FormFieldWrapper diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index 2491cc5..7dcdb67 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -12,13 +12,12 @@ import ListEditor from '../../components/ListEditor' import ListObjectFields from '../../components/ListObjectFields' import type { ResourcesRefs } from '../../types/Widget' +import FieldContainer from './FieldContainer' import AsyncSelect from './fields/AsyncSelect' import AutoComplete from './fields/AutoComplete' import type { FormWidgetData } from './Form' import styles from './Form.module.css' -import FormFieldWrapper from './FormFieldWrapper' import { evaluateVisibility, getOptionsFromEnum, isObjectSchema, isRecord } from './utils' -import VisibilityGate from './VisibilityGate' type FormGeneratorType = { descriptionTooltip: boolean @@ -422,49 +421,45 @@ const FormGenerator = ({ })() return ( - - shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath), - tooltip: descriptionTooltip && node.description ? node.description : undefined, - }} - id={name} - optionalHidden={optionalHidden} - required={required} - > - {formItemContent} - - + shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} + > + {formItemContent} + ) } case 'boolean': return ( - - shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), - tooltip: descriptionTooltip && node.description ? node.description : undefined, - valuePropName: 'checked', - }} - id={name} - optionalHidden={optionalHidden} - required={required} - > - - - + shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} + valuePropName='checked' + > + + ) @@ -504,23 +499,21 @@ const FormGenerator = ({ })() return ( - - shouldUpdateField(prev as string[], curr as string[], displayingDependencyPath), - tooltip: descriptionTooltip && node.description ? node.description : undefined, - }} - id={name} - optionalHidden={optionalHidden} - required={required} - > - {formItemContent} - - + shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} + > + {formItemContent} + ) } @@ -529,28 +522,26 @@ const FormGenerator = ({ const max = node.maximum return ( - - shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath), - tooltip: descriptionTooltip && node.description ? node.description : undefined, - }} - id={name} - optionalHidden={optionalHidden} - required={required} - > - {min && max && max - min < 100 ? ( - - ) : ( - - )} - - + shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} + > + {min && max && max - min < 100 ? ( + + ) : ( + + )} + ) } } diff --git a/src/widgets/Form/VisibilityGate.tsx b/src/widgets/Form/VisibilityGate.tsx deleted file mode 100644 index b308e3d..0000000 --- a/src/widgets/Form/VisibilityGate.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Form, type FormInstance } from 'antd' -import { useMemo } from 'react' - -import type { DisplayDependency } from './FormGenerator' -import { evaluateVisibility } from './utils' - -type VisibilityGateProps = { - children: React.ReactNode - dependency?: DisplayDependency - form: FormInstance -} - -const VisibilityGate = ({ - children, - dependency, - form, -}: VisibilityGateProps) => { - const watchedValue = Form.useWatch( - dependency?.dependsOn.name?.split('.') ?? [], - form, - ) as unknown - - const isVisible = useMemo(() => evaluateVisibility(dependency, watchedValue), [dependency, watchedValue]) - - if (!isVisible) { - return null - } - - return <>{children} -} - -export default VisibilityGate From a93c4bf614c789e96d9131869b9dab5403d3a0bb Mon Sep 17 00:00:00 2001 From: fedepini Date: Fri, 22 May 2026 16:35:17 +0200 Subject: [PATCH 14/16] refactor: updated FormGenerator --- src/widgets/Form/FormGenerator.tsx | 319 +++++++++++++---------------- 1 file changed, 137 insertions(+), 182 deletions(-) diff --git a/src/widgets/Form/FormGenerator.tsx b/src/widgets/Form/FormGenerator.tsx index 7dcdb67..e765c5c 100644 --- a/src/widgets/Form/FormGenerator.tsx +++ b/src/widgets/Form/FormGenerator.tsx @@ -327,193 +327,113 @@ const FormGenerator = ({ return (getInitialValue(prev, displayingDependencyPath) !== getInitialValue(curr, displayingDependencyPath)) } - const renderField = (label: string, name: string, node: JSONSchema4, required: boolean, formInstance?: FormInstance) => { - const currentForm = formInstance ?? form - - if (!node.type) { - console.error(`Field ${name} does not have a type in the schema or stringSchema`) - return null - } - - if (Array.isArray(node.type)) { - console.error(`type as array is not supported: ${node.type.join(',')} for field ${name}`) - return null - } - - const rules: Rule[] = [] - - if (required) { - rules.push({ message: 'Insert a value', required: true }) - } - - if (node.pattern) { - rules.push({ message: 'Insert right value', pattern: new RegExp(node.pattern) }) - } - - const displayingDependency = displayingDependenciesMap.get(name) - const displayingDependencyPath = displayingDependency?.dependsOn.name - + const renderFieldContent = ( + node: JSONSchema4, + name: string, + currentForm: FormInstance, + ) => { switch (node.type) { case 'string': { - const formItemContent = (() => { - // AsyncSelect - if (dependencies) { - const data = dependencies.find(field => field.name === name) + // AsyncSelect + if (dependencies) { + const data = dependencies.find(field => field.name === name) - if (data) { - return ( - - ) - } + if (data) { + return ( + + ) } + } - const options = getOptionsFromEnum(node.enum) + const options = getOptionsFromEnum(node.enum) - // Autocomplete - if (autocomplete) { - const data = autocomplete.find(field => field.name === name) + // Autocomplete + if (autocomplete) { + const data = autocomplete.find(field => field.name === name) - if (data) { - return ( - - ) - } + if (data) { + return ( + + ) } + } - // Enum - if (options) { - const currentValue = getInitialValue(transformedInitialValues, name) - const optionExists = options.some(({ value }) => String(value) === String(currentValue)) - - if (currentValue !== undefined && !optionExists) { - console.warn(`Invalid initial value for "${name}"`, currentValue) - form.setFieldValue(name.split('.'), undefined) - } + // Enum + if (options) { + const currentValue = getInitialValue(transformedInitialValues, name) + const optionExists = options.some(({ value }) => String(value) === String(currentValue)) - if (options.length > 4) { - return } - // Default - return - })() + return ( + + {options.map(({ label, value }) => ( + + {label} + + ))} + + ) + } - return ( - shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} - > - {formItemContent} - - ) + // Default + return } case 'boolean': return ( - - shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} - valuePropName='checked' - > - - - + ) case 'array': { - const formItemContent = (() => { - // objects - if (objectFields && node.items) { - const objFields = objectFields.find(({ path }) => path === name) - if (objFields) { - return ( - parseData(node.items as JSONSchema4, [], true, drawerForm)} - onChange={(values) => form.setFieldValue(name.split('.'), values)} - schema={node.items as JSONSchema4} - value={(form.getFieldValue(name.split('.')) as unknown[]) || []} - /> - ) - } - } - - // strings - const options = node.items && !Array.isArray(node.items) ? getOptionsFromEnum(node.items.enum) : undefined - if (options) { - return + } return ( - shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} - > - {formItemContent} - + { + currentForm.setFieldValue(name.split('.'), values) + }} + /> ) } @@ -522,31 +442,66 @@ const FormGenerator = ({ const max = node.maximum return ( - shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} - > - {min && max && max - min < 100 ? ( - - ) : ( - - )} - + <> + {min !== undefined && max !== undefined && max - min < 100 + ? + : + } + ) } } } + const renderField = (label: string, name: string, node: JSONSchema4, required: boolean, formInstance?: FormInstance) => { + const currentForm = formInstance ?? form + + if (!node.type) { + console.error(`Field ${name} does not have a type in the schema or stringSchema`) + return null + } + + if (Array.isArray(node.type)) { + console.error(`type as array is not supported: ${node.type.join(',')} for field ${name}`) + return null + } + + const rules: Rule[] = [] + + if (required) { + rules.push({ message: 'Insert a value', required: true }) + } + + if (node.pattern) { + rules.push({ message: 'Insert right value', pattern: new RegExp(node.pattern) }) + } + + const displayingDependency = displayingDependenciesMap.get(name) + const displayingDependencyPath = displayingDependency?.dependsOn.name + + const preserve = node.type === 'boolean' + + return ( + shouldUpdateField(prev as Store, curr as Store, displayingDependencyPath)} + valuePropName={node.type === 'boolean' ? 'checked' : undefined} + > + {renderFieldContent(node, name, currentForm)} + + ) + } + const getAnchorList = useCallback((values: Store): AnchorLinkItemProps[] => { const parseData = ( node: JSONSchema4, From 7c93f3d4343ba08cd528580ad06fb7a2a2a10a66 Mon Sep 17 00:00:00 2001 From: fedepini Date: Fri, 22 May 2026 16:40:57 +0200 Subject: [PATCH 15/16] chore: updated CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf78d53..defcf4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Previous changes not listed in this document can be traced using Git history. +## Unreleased + +### Added +- Added `displayingDependencies` property to `Form` widget to show / hide fields + ## [1.0.25] - 2026-05-12 ### Changed From 904e8a37f31dc0b3d2a58751b0e2a32617eb217a Mon Sep 17 00:00:00 2001 From: fedepini Date: Mon, 25 May 2026 12:09:04 +0200 Subject: [PATCH 16/16] refactor: updated examples --- src/examples/widgets/Form/Form.menu.yaml | 2 +- src/widgets/Form/fields/AsyncSelect.tsx | 15 +++++++++++++-- src/widgets/Form/fields/AutoComplete.tsx | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/examples/widgets/Form/Form.menu.yaml b/src/examples/widgets/Form/Form.menu.yaml index 8bfe706..8020a83 100644 --- a/src/examples/widgets/Form/Form.menu.yaml +++ b/src/examples/widgets/Form/Form.menu.yaml @@ -79,7 +79,6 @@ spec: allowedResources: - panels items: - - resourceRefId: example-form-displaying-dependencies-panel - resourceRefId: example-form-missing-schema-panel - resourceRefId: example-form-basic-strings-panel - resourceRefId: example-form-basic-menu-hidden-panel @@ -101,6 +100,7 @@ spec: - resourceRefId: example-form-autocomplete-restaction-parameter-panel - resourceRefId: example-form-dependencies-panel - resourceRefId: example-form-autocomplete-dependencies-array-objects-panel + - resourceRefId: example-form-displaying-dependencies-panel resourcesRefs: items: - id: example-form-missing-schema-panel diff --git a/src/widgets/Form/fields/AsyncSelect.tsx b/src/widgets/Form/fields/AsyncSelect.tsx index 060993c..25b118e 100644 --- a/src/widgets/Form/fields/AsyncSelect.tsx +++ b/src/widgets/Form/fields/AsyncSelect.tsx @@ -5,6 +5,7 @@ import useApp from 'antd/es/app/useApp' import type { DefaultOptionType } from 'antd/es/select' import { useEffect, useMemo, useRef } from 'react' +import { useAuth } from '../../../context/AuthContext' import { useConfigContext } from '../../../context/ConfigContext' import type { ResourcesRefs } from '../../../types/Widget' import type { FormWidgetData } from '../Form' @@ -21,6 +22,7 @@ type AsyncSelectProps = { const AsyncSelect = ({ data, form, initialValue, resourcesRefs }: AsyncSelectProps) => { const { notification } = useApp() const { config } = useConfigContext() + const { accessToken } = useAuth() const { dependsOn, extra: { key }, name, resourceRefId } = data @@ -56,10 +58,19 @@ const AsyncSelect = ({ data, form, initialValue, resourcesRefs }: AsyncSelectPro } }, [dependFieldValue, form, name]) + // eslint-disable-next-line @tanstack/query/exhaustive-deps const { data: options = [], isLoading } = useQuery({ enabled: !!(queryValue && config), - queryFn: () => getOptionsFromResourceRefId(queryValue as string, resourceRefId, resourcesRefs, key, notification, config), - queryKey: ['async-select-options', resourceRefId, dependFieldValue, key, queryValue, resourcesRefs, notification, config], + queryFn: () => getOptionsFromResourceRefId( + queryValue as string, + resourceRefId, + resourcesRefs, + key, + notification, + config, + accessToken + ), + queryKey: ['async-select-options', resourceRefId, dependFieldValue, key], refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000, }) diff --git a/src/widgets/Form/fields/AutoComplete.tsx b/src/widgets/Form/fields/AutoComplete.tsx index e11872b..c2caf92 100644 --- a/src/widgets/Form/fields/AutoComplete.tsx +++ b/src/widgets/Form/fields/AutoComplete.tsx @@ -47,11 +47,12 @@ const AutoComplete = ({ data, form, initialValue, options, resourcesRefs }: Auto return () => debouncedUpdate.cancel() }, [searchValue, debouncedUpdate]) + // eslint-disable-next-line @tanstack/query/exhaustive-deps const { data: queriedOptions = [], isLoading } = useQuery({ enabled: !!(queryValue && resourceRefId && config), queryFn: () => getOptionsFromResourceRefId(queryValue as string, resourceRefId, resourcesRefs, extra?.key, notification, config, accessToken), - queryKey: ['autocomplete-options', resourceRefId, queryValue, extra?.key, resourcesRefs, notification, config, accessToken], + queryKey: ['autocomplete-options', resourceRefId, queryValue, extra?.key], refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000, })