Skip to content

feat: Nested group-array and group-array conditional schema #131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@remoteoss/json-schema-form",
"version": "0.11.10-beta.0",
"version": "0.11.11-dev.20250220174843",
Copy link
Collaborator

@sandrina-p sandrina-p Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @SimoneErba! I'm doing the review now, it includes testing this PR against our internal E2E tests, therefore this new commit to release a dev version. Don't worry, I'll revert it once we are done.

(I'm about to close my day, so I'll finish the review on Monday, I hope that's okay.)

Copy link
Collaborator

@sandrina-p sandrina-p Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @SimoneErba , forgive us our delay, we didn't find the time to review this because of other pressing priorities.

Is it okay to wait another week or so for it?

If you need it sooner, you can use this dev release as a temporary solution while this PR is in review.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @sandrina-p yes no problem I will use the dev release

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @SimoneErba! Unfortunately the team didn't find the time to review your PR. The reason is because we are building the next major version of JSF, which will include your feat/bugfix, and we want to focus on shipping it as soon as possible.

In the meantime, the dev release should unblock you. We hope that's okay for now.
Thank you!

"description": "Headless UI form powered by JSON Schemas",
"author": "Remote.com <[email protected]> (https://remote.com/)",
"license": "MIT",
Expand Down
17 changes: 13 additions & 4 deletions src/createHeadlessForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ function buildFieldParameters(name, fieldProperties, required = [], config = {},
);
}

if (inputType === supportedTypes.GROUP_ARRAY) {
// eslint-disable-next-line no-use-before-define
fields = () =>
getFieldsFromJSONSchema(
fieldProperties.items,
{
customProperties: get(config, `customProperties.${name}.customProperties`, {}),
parentID: name,
},
logic
);
}

const result = {
name,
inputType,
Expand Down Expand Up @@ -242,7 +255,6 @@ function buildField(fieldParams, config, scopedJsonSchema, logic) {
const calculateConditionalFieldsClosure =
fieldParams.isDynamic &&
calculateConditionalProperties({ fieldParams, customProperties, logic, config });

const calculateCustomValidationPropertiesClosure = calculateCustomValidationProperties(
fieldParams,
customProperties
Expand Down Expand Up @@ -301,14 +313,11 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config, logic) {
if (fieldParams.inputType === 'group-array') {
const groupArrayItems = convertJSONSchemaPropertiesToFieldParameters(fieldParams.items);
const groupArrayFields = groupArrayItems.map((groupArrayItem) => {
groupArrayItem.nameKey = groupArrayItem.name;
const customProperties = null; // getCustomPropertiesForField(fieldParams, config); // TODO later support in group-array
const composeFn = getComposeFunctionForField(groupArrayItem, !!customProperties);
return composeFn(groupArrayItem);
});

fieldParams.nameKey = fieldParams.name;

fieldParams.nthFieldGroup = {
name: fieldParams.name,
label: fieldParams.label,
Expand Down
28 changes: 26 additions & 2 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ function hasType(type, typeName) {
* @returns
*/
export function getField(fieldName, fields) {
return fields.find(({ name }) => name === fieldName);
if (Array.isArray(fields)) {
return fields.find(({ name }) => name === fieldName);
} else {
return fields[fieldName];
}
}

/**
Expand All @@ -99,6 +103,7 @@ export function compareFormValueWithSchemaValue(formValue, schemaValue) {
// fallback to undefined since JSON-schemas empty values come represented as null
const currentPropertyValue =
typeof schemaValue === 'number' ? schemaValue : schemaValue || undefined;

// We're using the stringified version of both values since numeric values from forms come represented as Strings.
// By doing this, we're sure that we're comparing the same type.
return String(formValue) === String(currentPropertyValue);
Expand Down Expand Up @@ -333,7 +338,6 @@ export function processNode({
}) {
// Set initial required fields
const requiredFields = new Set(accRequired);

// Go through the node properties definition and update each field accordingly
Object.keys(node.properties ?? []).forEach((fieldName) => {
const field = getField(fieldName, formFields);
Expand Down Expand Up @@ -420,6 +424,26 @@ export function processNode({
logic,
});
}
if (inputType === supportedTypes.GROUP_ARRAY) {
// It's a group array, which might contain scoped conditions
const values = formValues[name];
if (Array.isArray(values)) {
const newFields = [];
const field = getField(name, formFields);
values.forEach((value) => {
const fields = field.fields();
processNode({
node: nestedNode.items,
formValues: value,
formFields: fields,
parentID: name,
logic,
});
newFields.push(fields);
});
field.dynamicFields = newFields;
}
}
});
}

Expand Down
1 change: 0 additions & 1 deletion src/internals/checkIfConditionMatches.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export function checkIfConditionMatchesProperties(node, formValues, formFields,
// https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else
return true;
}

if (hasProperty(currentProperty, 'const')) {
return compareFormValueWithSchemaValue(value, currentProperty.const);
}
Expand Down
227 changes: 227 additions & 0 deletions src/tests/conditions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,233 @@ describe('Conditional attributes updated', () => {
handleValidation({ is_full_time: 'no' });
expect(fields[1].visibilityCondition).toEqual(expect.any(Function));
});

it('Group array nested condition', () => {
const { fields, handleValidation } = createHeadlessForm({
type: 'object',
additionalProperties: false,
properties: {
companies: {
type: 'array',
'x-jsf-presentation': {
inputType: 'group-array',
},
items: {
type: 'object',
properties: {
company: {
type: 'string',
oneOf: [
{
title: 'A',
const: 'A',
},
{
title: 'B',
const: 'B',
},
],
'x-jsf-presentation': {
inputType: 'select',
},
},
role: {
title: 'Role',
oneOf: [],
'x-jsf-presentation': {
inputType: 'select',
},
},
},
allOf: [
{
if: {
properties: {
company: {
const: 'A',
},
},
required: ['company'],
},
then: {
properties: {
role: {
oneOf: [
{
title: 'adminA',
const: 'adminA',
},
{
title: 'userA',
const: 'userA',
},
],
'x-jsf-presentation': {
inputType: 'select',
},
},
},
required: ['role'],
},
},
{
if: {
properties: {
company: {
const: 'B',
},
},
required: ['company'],
},
then: {
properties: {
role: {
oneOf: [
{
title: 'adminB',
const: 'adminB',
},
{
title: 'userB',
const: 'userB',
},
],
'x-jsf-presentation': {
inputType: 'select',
},
},
},
required: ['role'],
},
},
],
required: ['company', 'role'],
},
},
},
required: ['companies'],
});

handleValidation({ companies: [{ company: 'A' }, { company: 'B' }] });
expect(fields[0].dynamicFields[0][1].options).toEqual([
{ label: 'adminA', value: 'adminA' },
{ label: 'userA', value: 'userA' },
]);
expect(fields[0].dynamicFields[1][1].options).toEqual([
{ label: 'adminB', value: 'adminB' },
{ label: 'userB', value: 'userB' },
]);
handleValidation({ companies: [] });
expect(fields[0].dynamicFields).toEqual([]);
});

it('select multiple conditions', () => {
const { fields, handleValidation } = createHeadlessForm({
type: 'object',
additionalProperties: false,
properties: {
company: {
title: 'Select Company',
type: 'string',
description: 'Choose a company',
oneOf: [
{
title: 'A',
const: 'A',
},
{
title: 'B',
const: 'B',
},
],
'x-jsf-presentation': {
inputType: 'select',
},
},
role: {
title: 'Role',
oneOf: [],
'x-jsf-presentation': {
inputType: 'select',
},
},
},
allOf: [
{
if: {
properties: {
company: {
const: 'A',
},
},
required: ['company'],
},
then: {
properties: {
role: {
oneOf: [
{
title: 'adminA',
const: 'adminA',
},
{
title: 'userA',
const: 'userA',
},
],
'x-jsf-presentation': {
inputType: 'select',
},
},
},
required: ['role'],
},
},
{
if: {
properties: {
company: {
const: 'B',
},
},
required: ['company'],
},
then: {
properties: {
role: {
oneOf: [
{
title: 'adminB',
const: 'adminB',
},
{
title: 'userB',
const: 'userB',
},
],
'x-jsf-presentation': {
inputType: 'select',
},
},
},
required: ['role'],
},
},
],
required: ['company', 'role'],
});

handleValidation({ company: 'A' });
expect(fields[1].options).toEqual([
{ label: 'adminA', value: 'adminA' },
{ label: 'userA', value: 'userA' },
]);
handleValidation({ company: 'B' });
expect(fields[1].options).toEqual([
{ label: 'adminB', value: 'adminB' },
{ label: 'userB', value: 'userB' },
]);
});
});

describe('Conditional with a minimum value check', () => {
Expand Down
Loading