@remoteoss/json-schema-form 0.12.0-beta.0 → 0.12.1-dev.20250929100600

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2025 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.12.0-beta.0
5
- Generated: Wed, 25 Jun 2025 12:45:39 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.12.1-dev.20250929100600
5
+ Generated: Mon, 29 Sep 2025 10:07:37 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -27,6 +27,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
27
  SOFTWARE.
28
28
 
29
29
  */
30
+ "use strict";
30
31
  var __create = Object.create;
31
32
  var __defProp = Object.defineProperty;
32
33
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -736,9 +737,7 @@ function buildYupSchema(field, config, logic) {
736
737
  const fieldSetShape = {};
737
738
  innerFields.forEach((fieldSetfield) => {
738
739
  if (fieldSetfield.fields) {
739
- fieldSetShape[fieldSetfield.name] = (0, import_yup.object)().shape(
740
- buildFieldSetSchema(fieldSetfield.fields)
741
- );
740
+ fieldSetShape[fieldSetfield.name] = (0, import_yup.object)().shape(buildFieldSetSchema(fieldSetfield.fields)).nullable();
742
741
  } else {
743
742
  fieldSetShape[fieldSetfield.name] = buildYupSchema(
744
743
  {
@@ -1065,20 +1064,26 @@ function validateInlineRules(jsonSchema, sampleEmptyObject) {
1065
1064
  });
1066
1065
  });
1067
1066
  }
1068
- function checkRuleIntegrity(rule, id, data, errorMessage = (item) => `[json-schema-form] json-logic error: rule "${id}" has no variable "${item.var}".`) {
1067
+ function checkRuleIntegrity(rule, id, data, errorMessage = (item) => `[json-schema-form] json-logic error: rule "${id}" has no variable "${item.var}".`, inReduceOrMap = false) {
1069
1068
  Object.entries(rule ?? {}).map(([operator, subRule]) => {
1070
1069
  if (!Array.isArray(subRule) && subRule !== null && subRule !== void 0)
1071
1070
  return;
1072
1071
  throwIfUnknownOperator(operator, subRule, id);
1072
+ const isReduceOrMap = inReduceOrMap || operator === "reduce" || operator === "map";
1073
+ const validationData = isReduceOrMap ? {
1074
+ ...data,
1075
+ accumulator: 0,
1076
+ current: null
1077
+ } : data;
1073
1078
  subRule.map((item) => {
1074
1079
  const isVar = item !== null && typeof item === "object" && Object.hasOwn(item, "var");
1075
1080
  if (isVar) {
1076
- const exists = import_json_logic_js.default.apply({ var: removeIndicesFromPath(item.var) }, data);
1081
+ const exists = import_json_logic_js.default.apply({ var: removeIndicesFromPath(item.var) }, validationData);
1077
1082
  if (exists === null) {
1078
1083
  throw Error(errorMessage(item));
1079
1084
  }
1080
1085
  } else {
1081
- checkRuleIntegrity(item, id, data);
1086
+ checkRuleIntegrity(item, id, data, errorMessage, isReduceOrMap);
1082
1087
  }
1083
1088
  });
1084
1089
  });
@@ -1259,6 +1264,30 @@ function getPrefillValues(fields, initialValues = {}) {
1259
1264
  });
1260
1265
  return initialValues;
1261
1266
  }
1267
+ function preserveNestedFieldsVisibility(field, parentPath = "") {
1268
+ return field.fields?.reduce?.((acc, f) => {
1269
+ const path = parentPath ? `${parentPath}.${f.name}` : f.name;
1270
+ if (!(0, import_isNil.default)(f.isVisible)) {
1271
+ acc[path] = f.isVisible;
1272
+ }
1273
+ if (f.fields) {
1274
+ Object.assign(acc, preserveNestedFieldsVisibility(f, path));
1275
+ }
1276
+ return acc;
1277
+ }, {});
1278
+ }
1279
+ function restoreNestedFieldsVisibility(field, nestedFieldsVisibility, parentPath = "") {
1280
+ field.fields.forEach((f) => {
1281
+ const path = parentPath ? `${parentPath}.${f.name}` : f.name;
1282
+ const visibility = (0, import_get2.default)(nestedFieldsVisibility, path);
1283
+ if (!(0, import_isNil.default)(visibility)) {
1284
+ f.isVisible = visibility;
1285
+ }
1286
+ if (f.fields) {
1287
+ restoreNestedFieldsVisibility(f, nestedFieldsVisibility, path);
1288
+ }
1289
+ });
1290
+ }
1262
1291
  function updateField(field, requiredFields, node, formValues, logic, config) {
1263
1292
  if (!field) {
1264
1293
  return;
@@ -1270,9 +1299,13 @@ function updateField(field, requiredFields, node, formValues, logic, config) {
1270
1299
  if (fieldIsRequired) {
1271
1300
  field.isVisible = true;
1272
1301
  }
1302
+ const nestedFieldsVisibility = preserveNestedFieldsVisibility(field);
1273
1303
  const updateAttributes = (fieldAttrs) => {
1274
1304
  Object.entries(fieldAttrs).forEach(([key, value]) => {
1275
1305
  field[key] = value;
1306
+ if (key === "fields" && !(0, import_isNil.default)(nestedFieldsVisibility)) {
1307
+ restoreNestedFieldsVisibility(field, nestedFieldsVisibility);
1308
+ }
1276
1309
  if (key === "schema" && typeof value === "function") {
1277
1310
  field[key] = value();
1278
1311
  }
@@ -1324,9 +1357,19 @@ function processNode({
1324
1357
  logic
1325
1358
  }) {
1326
1359
  const requiredFields = new Set(accRequired);
1327
- Object.keys(node.properties ?? []).forEach((fieldName) => {
1360
+ Object.entries(node.properties ?? []).forEach(([fieldName, nestedNode]) => {
1328
1361
  const field = getField(fieldName, formFields);
1329
1362
  updateField(field, requiredFields, node, formValues, logic, { parentID });
1363
+ const isFieldset = field?.inputType === supportedTypes.FIELDSET;
1364
+ if (isFieldset) {
1365
+ processNode({
1366
+ node: nestedNode,
1367
+ formValues: formValues[fieldName] || {},
1368
+ formFields: field.fields,
1369
+ parentID,
1370
+ logic
1371
+ });
1372
+ }
1330
1373
  });
1331
1374
  node.required?.forEach((fieldName) => {
1332
1375
  requiredFields.add(fieldName);
@@ -1384,20 +1427,6 @@ function processNode({
1384
1427
  allOfItemRequired.forEach(requiredFields.add, requiredFields);
1385
1428
  });
1386
1429
  }
1387
- if (node.properties) {
1388
- Object.entries(node.properties).forEach(([name, nestedNode]) => {
1389
- const inputType = getInputType(nestedNode);
1390
- if (inputType === supportedTypes.FIELDSET) {
1391
- processNode({
1392
- node: nestedNode,
1393
- formValues: formValues[name] || {},
1394
- formFields: getField(name, formFields).fields,
1395
- parentID: name,
1396
- logic
1397
- });
1398
- }
1399
- });
1400
- }
1401
1430
  if (node["x-jsf-logic"]) {
1402
1431
  const { required: requiredFromLogic } = processJSONLogicNode({
1403
1432
  node: node["x-jsf-logic"],
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2025 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.12.0-beta.0
5
- Generated: Wed, 25 Jun 2025 12:45:39 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.12.1-dev.20250929100600
5
+ Generated: Mon, 29 Sep 2025 10:07:37 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -699,9 +699,7 @@ function buildYupSchema(field, config, logic) {
699
699
  const fieldSetShape = {};
700
700
  innerFields.forEach((fieldSetfield) => {
701
701
  if (fieldSetfield.fields) {
702
- fieldSetShape[fieldSetfield.name] = object().shape(
703
- buildFieldSetSchema(fieldSetfield.fields)
704
- );
702
+ fieldSetShape[fieldSetfield.name] = object().shape(buildFieldSetSchema(fieldSetfield.fields)).nullable();
705
703
  } else {
706
704
  fieldSetShape[fieldSetfield.name] = buildYupSchema(
707
705
  {
@@ -1028,20 +1026,26 @@ function validateInlineRules(jsonSchema, sampleEmptyObject) {
1028
1026
  });
1029
1027
  });
1030
1028
  }
1031
- function checkRuleIntegrity(rule, id, data, errorMessage = (item) => `[json-schema-form] json-logic error: rule "${id}" has no variable "${item.var}".`) {
1029
+ function checkRuleIntegrity(rule, id, data, errorMessage = (item) => `[json-schema-form] json-logic error: rule "${id}" has no variable "${item.var}".`, inReduceOrMap = false) {
1032
1030
  Object.entries(rule ?? {}).map(([operator, subRule]) => {
1033
1031
  if (!Array.isArray(subRule) && subRule !== null && subRule !== void 0)
1034
1032
  return;
1035
1033
  throwIfUnknownOperator(operator, subRule, id);
1034
+ const isReduceOrMap = inReduceOrMap || operator === "reduce" || operator === "map";
1035
+ const validationData = isReduceOrMap ? {
1036
+ ...data,
1037
+ accumulator: 0,
1038
+ current: null
1039
+ } : data;
1036
1040
  subRule.map((item) => {
1037
1041
  const isVar = item !== null && typeof item === "object" && Object.hasOwn(item, "var");
1038
1042
  if (isVar) {
1039
- const exists = jsonLogic.apply({ var: removeIndicesFromPath(item.var) }, data);
1043
+ const exists = jsonLogic.apply({ var: removeIndicesFromPath(item.var) }, validationData);
1040
1044
  if (exists === null) {
1041
1045
  throw Error(errorMessage(item));
1042
1046
  }
1043
1047
  } else {
1044
- checkRuleIntegrity(item, id, data);
1048
+ checkRuleIntegrity(item, id, data, errorMessage, isReduceOrMap);
1045
1049
  }
1046
1050
  });
1047
1051
  });
@@ -1222,6 +1226,30 @@ function getPrefillValues(fields, initialValues = {}) {
1222
1226
  });
1223
1227
  return initialValues;
1224
1228
  }
1229
+ function preserveNestedFieldsVisibility(field, parentPath = "") {
1230
+ return field.fields?.reduce?.((acc, f) => {
1231
+ const path = parentPath ? `${parentPath}.${f.name}` : f.name;
1232
+ if (!isNil(f.isVisible)) {
1233
+ acc[path] = f.isVisible;
1234
+ }
1235
+ if (f.fields) {
1236
+ Object.assign(acc, preserveNestedFieldsVisibility(f, path));
1237
+ }
1238
+ return acc;
1239
+ }, {});
1240
+ }
1241
+ function restoreNestedFieldsVisibility(field, nestedFieldsVisibility, parentPath = "") {
1242
+ field.fields.forEach((f) => {
1243
+ const path = parentPath ? `${parentPath}.${f.name}` : f.name;
1244
+ const visibility = get2(nestedFieldsVisibility, path);
1245
+ if (!isNil(visibility)) {
1246
+ f.isVisible = visibility;
1247
+ }
1248
+ if (f.fields) {
1249
+ restoreNestedFieldsVisibility(f, nestedFieldsVisibility, path);
1250
+ }
1251
+ });
1252
+ }
1225
1253
  function updateField(field, requiredFields, node, formValues, logic, config) {
1226
1254
  if (!field) {
1227
1255
  return;
@@ -1233,9 +1261,13 @@ function updateField(field, requiredFields, node, formValues, logic, config) {
1233
1261
  if (fieldIsRequired) {
1234
1262
  field.isVisible = true;
1235
1263
  }
1264
+ const nestedFieldsVisibility = preserveNestedFieldsVisibility(field);
1236
1265
  const updateAttributes = (fieldAttrs) => {
1237
1266
  Object.entries(fieldAttrs).forEach(([key, value]) => {
1238
1267
  field[key] = value;
1268
+ if (key === "fields" && !isNil(nestedFieldsVisibility)) {
1269
+ restoreNestedFieldsVisibility(field, nestedFieldsVisibility);
1270
+ }
1239
1271
  if (key === "schema" && typeof value === "function") {
1240
1272
  field[key] = value();
1241
1273
  }
@@ -1287,9 +1319,19 @@ function processNode({
1287
1319
  logic
1288
1320
  }) {
1289
1321
  const requiredFields = new Set(accRequired);
1290
- Object.keys(node.properties ?? []).forEach((fieldName) => {
1322
+ Object.entries(node.properties ?? []).forEach(([fieldName, nestedNode]) => {
1291
1323
  const field = getField(fieldName, formFields);
1292
1324
  updateField(field, requiredFields, node, formValues, logic, { parentID });
1325
+ const isFieldset = field?.inputType === supportedTypes.FIELDSET;
1326
+ if (isFieldset) {
1327
+ processNode({
1328
+ node: nestedNode,
1329
+ formValues: formValues[fieldName] || {},
1330
+ formFields: field.fields,
1331
+ parentID,
1332
+ logic
1333
+ });
1334
+ }
1293
1335
  });
1294
1336
  node.required?.forEach((fieldName) => {
1295
1337
  requiredFields.add(fieldName);
@@ -1347,20 +1389,6 @@ function processNode({
1347
1389
  allOfItemRequired.forEach(requiredFields.add, requiredFields);
1348
1390
  });
1349
1391
  }
1350
- if (node.properties) {
1351
- Object.entries(node.properties).forEach(([name, nestedNode]) => {
1352
- const inputType = getInputType(nestedNode);
1353
- if (inputType === supportedTypes.FIELDSET) {
1354
- processNode({
1355
- node: nestedNode,
1356
- formValues: formValues[name] || {},
1357
- formFields: getField(name, formFields).fields,
1358
- parentID: name,
1359
- logic
1360
- });
1361
- }
1362
- });
1363
- }
1364
1392
  if (node["x-jsf-logic"]) {
1365
1393
  const { required: requiredFromLogic } = processJSONLogicNode({
1366
1394
  node: node["x-jsf-logic"],
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2025 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.12.0-beta.0
5
- Generated: Wed, 25 Jun 2025 12:45:39 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.12.1-dev.20250929100600
5
+ Generated: Mon, 29 Sep 2025 10:07:37 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -12215,9 +12215,7 @@ function buildYupSchema(field, config, logic) {
12215
12215
  const fieldSetShape = {};
12216
12216
  innerFields.forEach((fieldSetfield) => {
12217
12217
  if (fieldSetfield.fields) {
12218
- fieldSetShape[fieldSetfield.name] = ObjectSchema().shape(
12219
- buildFieldSetSchema(fieldSetfield.fields)
12220
- );
12218
+ fieldSetShape[fieldSetfield.name] = ObjectSchema().shape(buildFieldSetSchema(fieldSetfield.fields)).nullable();
12221
12219
  } else {
12222
12220
  fieldSetShape[fieldSetfield.name] = buildYupSchema(
12223
12221
  {
@@ -12544,20 +12542,26 @@ function validateInlineRules(jsonSchema, sampleEmptyObject) {
12544
12542
  });
12545
12543
  });
12546
12544
  }
12547
- function checkRuleIntegrity(rule, id, data, errorMessage = (item) => `[json-schema-form] json-logic error: rule "${id}" has no variable "${item.var}".`) {
12545
+ function checkRuleIntegrity(rule, id, data, errorMessage = (item) => `[json-schema-form] json-logic error: rule "${id}" has no variable "${item.var}".`, inReduceOrMap = false) {
12548
12546
  Object.entries(rule ?? {}).map(([operator, subRule]) => {
12549
12547
  if (!Array.isArray(subRule) && subRule !== null && subRule !== void 0)
12550
12548
  return;
12551
12549
  throwIfUnknownOperator(operator, subRule, id);
12550
+ const isReduceOrMap = inReduceOrMap || operator === "reduce" || operator === "map";
12551
+ const validationData = isReduceOrMap ? {
12552
+ ...data,
12553
+ accumulator: 0,
12554
+ current: null
12555
+ } : data;
12552
12556
  subRule.map((item) => {
12553
12557
  const isVar = item !== null && typeof item === "object" && Object.hasOwn(item, "var");
12554
12558
  if (isVar) {
12555
- const exists = import_json_logic_js.default.apply({ var: removeIndicesFromPath(item.var) }, data);
12559
+ const exists = import_json_logic_js.default.apply({ var: removeIndicesFromPath(item.var) }, validationData);
12556
12560
  if (exists === null) {
12557
12561
  throw Error(errorMessage(item));
12558
12562
  }
12559
12563
  } else {
12560
- checkRuleIntegrity(item, id, data);
12564
+ checkRuleIntegrity(item, id, data, errorMessage, isReduceOrMap);
12561
12565
  }
12562
12566
  });
12563
12567
  });
@@ -12738,6 +12742,30 @@ function getPrefillValues(fields, initialValues = {}) {
12738
12742
  });
12739
12743
  return initialValues;
12740
12744
  }
12745
+ function preserveNestedFieldsVisibility(field, parentPath = "") {
12746
+ return field.fields?.reduce?.((acc, f) => {
12747
+ const path = parentPath ? `${parentPath}.${f.name}` : f.name;
12748
+ if (!(0, import_isNil.default)(f.isVisible)) {
12749
+ acc[path] = f.isVisible;
12750
+ }
12751
+ if (f.fields) {
12752
+ Object.assign(acc, preserveNestedFieldsVisibility(f, path));
12753
+ }
12754
+ return acc;
12755
+ }, {});
12756
+ }
12757
+ function restoreNestedFieldsVisibility(field, nestedFieldsVisibility, parentPath = "") {
12758
+ field.fields.forEach((f) => {
12759
+ const path = parentPath ? `${parentPath}.${f.name}` : f.name;
12760
+ const visibility = (0, import_get3.default)(nestedFieldsVisibility, path);
12761
+ if (!(0, import_isNil.default)(visibility)) {
12762
+ f.isVisible = visibility;
12763
+ }
12764
+ if (f.fields) {
12765
+ restoreNestedFieldsVisibility(f, nestedFieldsVisibility, path);
12766
+ }
12767
+ });
12768
+ }
12741
12769
  function updateField(field, requiredFields, node, formValues, logic, config) {
12742
12770
  if (!field) {
12743
12771
  return;
@@ -12749,9 +12777,13 @@ function updateField(field, requiredFields, node, formValues, logic, config) {
12749
12777
  if (fieldIsRequired) {
12750
12778
  field.isVisible = true;
12751
12779
  }
12780
+ const nestedFieldsVisibility = preserveNestedFieldsVisibility(field);
12752
12781
  const updateAttributes = (fieldAttrs) => {
12753
12782
  Object.entries(fieldAttrs).forEach(([key, value]) => {
12754
12783
  field[key] = value;
12784
+ if (key === "fields" && !(0, import_isNil.default)(nestedFieldsVisibility)) {
12785
+ restoreNestedFieldsVisibility(field, nestedFieldsVisibility);
12786
+ }
12755
12787
  if (key === "schema" && typeof value === "function") {
12756
12788
  field[key] = value();
12757
12789
  }
@@ -12803,9 +12835,19 @@ function processNode({
12803
12835
  logic
12804
12836
  }) {
12805
12837
  const requiredFields = new Set(accRequired);
12806
- Object.keys(node.properties ?? []).forEach((fieldName) => {
12838
+ Object.entries(node.properties ?? []).forEach(([fieldName, nestedNode]) => {
12807
12839
  const field = getField(fieldName, formFields);
12808
12840
  updateField(field, requiredFields, node, formValues, logic, { parentID });
12841
+ const isFieldset = field?.inputType === supportedTypes.FIELDSET;
12842
+ if (isFieldset) {
12843
+ processNode({
12844
+ node: nestedNode,
12845
+ formValues: formValues[fieldName] || {},
12846
+ formFields: field.fields,
12847
+ parentID,
12848
+ logic
12849
+ });
12850
+ }
12809
12851
  });
12810
12852
  node.required?.forEach((fieldName) => {
12811
12853
  requiredFields.add(fieldName);
@@ -12863,20 +12905,6 @@ function processNode({
12863
12905
  allOfItemRequired.forEach(requiredFields.add, requiredFields);
12864
12906
  });
12865
12907
  }
12866
- if (node.properties) {
12867
- Object.entries(node.properties).forEach(([name, nestedNode]) => {
12868
- const inputType = getInputType(nestedNode);
12869
- if (inputType === supportedTypes.FIELDSET) {
12870
- processNode({
12871
- node: nestedNode,
12872
- formValues: formValues[name] || {},
12873
- formFields: getField(name, formFields).fields,
12874
- parentID: name,
12875
- logic
12876
- });
12877
- }
12878
- });
12879
- }
12880
12908
  if (node["x-jsf-logic"]) {
12881
12909
  const { required: requiredFromLogic } = processJSONLogicNode({
12882
12910
  node: node["x-jsf-logic"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remoteoss/json-schema-form",
3
- "version": "0.12.0-beta.0",
3
+ "version": "0.12.1-dev.20250929100600",
4
4
  "description": "Headless UI form powered by JSON Schemas",
5
5
  "author": "Remote.com <engineering@remote.com> (https://remote.com/)",
6
6
  "license": "MIT",
@@ -27,26 +27,23 @@
27
27
  "test:watch": "jest --watchAll",
28
28
  "lint": "eslint \"src/**/*.{js,ts}\"",
29
29
  "format": "npm run format:prettier && npm run format:eslint",
30
- "prepare": "husky install",
30
+ "prepare": "cd .. && husky install",
31
31
  "format:eslint": "eslint --fix \"src/**/*.{js,ts}\"",
32
32
  "format:prettier": "prettier --write \"src/**/*.{js,ts}\"",
33
33
  "prettier:check": "prettier --check \"src/**/*.{js,ts}\"",
34
34
  "check:pr-version": "node scripts/pr_dev_version",
35
- "check:pr-next-version": "node scripts/pr_next_dev_version",
36
35
  "release:local": "node scripts/release_local",
37
36
  "release:dev:patch": "node scripts/release_dev patch",
38
37
  "release:dev:minor": "node scripts/release_dev minor",
39
38
  "release:main:patch": "node scripts/release_main patch",
40
39
  "release:main:minor": "node scripts/release_main minor",
41
- "release:v1:dev": "node scripts/release_v1 dev",
42
- "release:v1:beta": "node scripts/release_v1 beta",
43
40
  "version_as_main": "node scripts/version_as_main.js",
44
41
  "psrepublishOnly": "if [[ ! $PWD =~ scripts$ ]]; then npm run publish:nopublish; fi",
45
42
  "psublish:nopublish": "echo 'Use `npm release:*` script instead && exit 1"
46
43
  },
47
44
  "lint-staged": {
48
45
  "*.{js,jsx}": [
49
- "npm run format"
46
+ "npm run format --prefix ./v0"
50
47
  ]
51
48
  },
52
49
  "dependencies": {
@@ -63,6 +63,7 @@ import {
63
63
  schemaForErrorMessageSpecificity,
64
64
  jsfConfigForErrorMessageSpecificity,
65
65
  schemaInputTypeFile,
66
+ schemaWithNestedFieldsetsConditionals,
66
67
  } from './helpers';
67
68
  import { mockConsole, restoreConsoleAndEnsureItWasNotCalled } from './testUtils';
68
69
  import { createHeadlessForm } from '@/createHeadlessForm';
@@ -2200,6 +2201,112 @@ describe('createHeadlessForm', () => {
2200
2201
  ).toBeUndefined();
2201
2202
  });
2202
2203
  });
2204
+
2205
+ describe('supports conditionals over nested fieldsets', () => {
2206
+ it('retirement_plan fieldset is hidden when no values are provided', () => {
2207
+ const { fields, handleValidation } = createHeadlessForm(
2208
+ schemaWithNestedFieldsetsConditionals,
2209
+ {}
2210
+ );
2211
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
2212
+
2213
+ expect(getField(fields, 'perks', 'retirement_plan').isVisible).toBe(false);
2214
+
2215
+ expect(validateForm({ perks: {} })).toEqual({
2216
+ perks: {
2217
+ benefits_package: 'Required field',
2218
+ has_retirement_plan: 'Required field',
2219
+ },
2220
+ });
2221
+
2222
+ expect(getField(fields, 'perks', 'retirement_plan').isVisible).toBe(false);
2223
+ });
2224
+
2225
+ it("submits without retirement_plan when user selects 'no' for has_retirement_plan", () => {
2226
+ const { fields, handleValidation } = createHeadlessForm(
2227
+ schemaWithNestedFieldsetsConditionals,
2228
+ {}
2229
+ );
2230
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
2231
+
2232
+ expect(
2233
+ validateForm({ perks: { benefits_package: 'basic', has_retirement_plan: 'no' } })
2234
+ ).toBeUndefined();
2235
+
2236
+ expect(getField(fields, 'perks', 'retirement_plan').isVisible).toBe(false);
2237
+ });
2238
+
2239
+ it("retirement_plan fieldset is visible when user selects 'yes' for has_retirement_plan", () => {
2240
+ const { fields, handleValidation } = createHeadlessForm(
2241
+ schemaWithNestedFieldsetsConditionals,
2242
+ {}
2243
+ );
2244
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
2245
+
2246
+ expect(
2247
+ validateForm({
2248
+ perks: {
2249
+ benefits_package: 'basic',
2250
+ has_retirement_plan: 'yes',
2251
+ declare_amount: 'yes',
2252
+ retirement_plan: { plan_name: 'test', year: 2025 },
2253
+ },
2254
+ })
2255
+ ).toEqual({
2256
+ perks: {
2257
+ retirement_plan: {
2258
+ amount: 'Required field',
2259
+ },
2260
+ },
2261
+ });
2262
+
2263
+ expect(getField(fields, 'perks', 'retirement_plan').isVisible).toBe(true);
2264
+ expect(getField(fields, 'perks', 'declare_amount').isVisible).toBe(true);
2265
+ expect(getField(fields, 'perks', 'declare_amount').default).toBe('yes');
2266
+ expect(getField(fields, 'perks', 'retirement_plan', 'amount').isVisible).toBe(true);
2267
+ });
2268
+
2269
+ it("retirement_plan's amount field is hidden when user selects 'no' for declare_amount", () => {
2270
+ const { fields, handleValidation } = createHeadlessForm(
2271
+ schemaWithNestedFieldsetsConditionals,
2272
+ {}
2273
+ );
2274
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
2275
+
2276
+ expect(
2277
+ validateForm({
2278
+ perks: {
2279
+ benefits_package: 'basic',
2280
+ has_retirement_plan: 'yes',
2281
+ declare_amount: 'no',
2282
+ retirement_plan: { plan_name: 'test', year: 2025 },
2283
+ },
2284
+ })
2285
+ ).toBeUndefined();
2286
+
2287
+ expect(getField(fields, 'perks', 'retirement_plan').isVisible).toBe(true);
2288
+ expect(getField(fields, 'perks', 'declare_amount').isVisible).toBe(true);
2289
+ expect(getField(fields, 'perks', 'retirement_plan', 'amount').isVisible).toBe(false);
2290
+ });
2291
+
2292
+ it('submits with valid retirement_plan', async () => {
2293
+ const { handleValidation } = createHeadlessForm(
2294
+ schemaWithNestedFieldsetsConditionals,
2295
+ {}
2296
+ );
2297
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
2298
+
2299
+ expect(
2300
+ validateForm({
2301
+ perks: {
2302
+ benefits_package: 'plus',
2303
+ has_retirement_plan: 'yes',
2304
+ retirement_plan: { plan_name: 'test', year: 2025, amount: 1000 },
2305
+ },
2306
+ })
2307
+ ).toBeUndefined();
2308
+ });
2309
+ });
2203
2310
  });
2204
2311
 
2205
2312
  it('support "email" field type', () => {
@@ -1939,7 +1939,6 @@ export const schemaWithConditionalToFieldset = {
1939
1939
  then: {
1940
1940
  properties: {
1941
1941
  pto: {
1942
- $comment: '@BUG: This description does not disappear once activated.',
1943
1942
  description: 'Above 30 hours, the PTO needs to be at least 20 days.',
1944
1943
  minimum: 20,
1945
1944
  },
@@ -1972,6 +1971,178 @@ export const schemaWithConditionalToFieldset = {
1972
1971
  required: ['perks', 'work_hours_per_week'],
1973
1972
  };
1974
1973
 
1974
+ export const schemaWithNestedFieldsetsConditionals = {
1975
+ additionalProperties: false,
1976
+ type: 'object',
1977
+ properties: {
1978
+ perks: {
1979
+ additionalProperties: false,
1980
+ properties: {
1981
+ benefits_package: {
1982
+ oneOf: [
1983
+ {
1984
+ const: 'basic',
1985
+ title: 'Basic',
1986
+ },
1987
+ {
1988
+ const: 'plus',
1989
+ title: 'Plus',
1990
+ },
1991
+ ],
1992
+ title: 'Benefits package',
1993
+ type: 'string',
1994
+ 'x-jsf-presentation': {
1995
+ inputType: 'radio',
1996
+ },
1997
+ },
1998
+ has_retirement_plan: {
1999
+ oneOf: [
2000
+ {
2001
+ const: 'yes',
2002
+ title: 'Yes',
2003
+ },
2004
+ {
2005
+ const: 'no',
2006
+ title: 'No',
2007
+ },
2008
+ ],
2009
+ title: 'Has retirement plan?',
2010
+ type: 'string',
2011
+ 'x-jsf-presentation': {
2012
+ inputType: 'radio',
2013
+ },
2014
+ },
2015
+ declare_amount: {
2016
+ oneOf: [
2017
+ {
2018
+ const: 'yes',
2019
+ title: 'Yes',
2020
+ },
2021
+ {
2022
+ const: 'no',
2023
+ title: 'No',
2024
+ },
2025
+ ],
2026
+ title: 'Declare retirement amount?',
2027
+ type: 'string',
2028
+ default: 'yes',
2029
+ 'x-jsf-presentation': {
2030
+ inputType: 'radio',
2031
+ },
2032
+ },
2033
+ retirement_plan: {
2034
+ type: 'object',
2035
+ title: 'Retirement plan',
2036
+ properties: {
2037
+ plan_name: {
2038
+ type: 'string',
2039
+ title: 'Plan name',
2040
+ },
2041
+ year: {
2042
+ type: 'number',
2043
+ title: 'Year',
2044
+ },
2045
+ amount: {
2046
+ type: 'number',
2047
+ title: 'Amount',
2048
+ },
2049
+ },
2050
+ required: ['plan_name', 'year'],
2051
+ 'x-jsf-presentation': {
2052
+ inputType: 'fieldset',
2053
+ },
2054
+ },
2055
+ },
2056
+ required: ['benefits_package', 'has_retirement_plan'],
2057
+ title: 'Perks',
2058
+ type: 'object',
2059
+ 'x-jsf-presentation': {
2060
+ inputType: 'fieldset',
2061
+ },
2062
+ },
2063
+ },
2064
+ allOf: [
2065
+ {
2066
+ if: {
2067
+ properties: {
2068
+ perks: {
2069
+ properties: {
2070
+ has_retirement_plan: {
2071
+ const: 'no',
2072
+ },
2073
+ },
2074
+ },
2075
+ },
2076
+ },
2077
+ then: {
2078
+ properties: {
2079
+ perks: {
2080
+ properties: {
2081
+ retirement_plan: false,
2082
+ declare_amount: false,
2083
+ },
2084
+ },
2085
+ },
2086
+ },
2087
+ },
2088
+ {
2089
+ if: {
2090
+ properties: {
2091
+ perks: {
2092
+ properties: {
2093
+ declare_amount: {
2094
+ const: 'no',
2095
+ },
2096
+ },
2097
+ required: ['declare_amount'],
2098
+ },
2099
+ },
2100
+ },
2101
+ then: {
2102
+ properties: {
2103
+ perks: {
2104
+ properties: {
2105
+ retirement_plan: {
2106
+ properties: {
2107
+ amount: false,
2108
+ },
2109
+ },
2110
+ },
2111
+ },
2112
+ },
2113
+ },
2114
+ },
2115
+ {
2116
+ if: {
2117
+ properties: {
2118
+ perks: {
2119
+ properties: {
2120
+ has_retirement_plan: {
2121
+ const: 'yes',
2122
+ },
2123
+ declare_amount: {
2124
+ const: 'yes',
2125
+ },
2126
+ },
2127
+ required: ['has_retirement_plan', 'declare_amount'],
2128
+ },
2129
+ },
2130
+ },
2131
+ then: {
2132
+ properties: {
2133
+ perks: {
2134
+ properties: {
2135
+ retirement_plan: {
2136
+ required: ['amount'],
2137
+ },
2138
+ },
2139
+ },
2140
+ },
2141
+ },
2142
+ },
2143
+ ],
2144
+ };
2145
+
1975
2146
  export const schemaWorkSchedule = {
1976
2147
  type: 'object',
1977
2148
  properties: {
@@ -1061,7 +1061,7 @@ export const schemaWithReduceAccumulator = {
1061
1061
  type: 'number',
1062
1062
  'x-jsf-logic-computedAttrs': {
1063
1063
  const: 'computed_work_hours_per_week',
1064
- defaultValue: 'computed_work_hours_per_week',
1064
+ default: 'computed_work_hours_per_week',
1065
1065
  title: '{{computed_work_hours_per_week}} hours per week',
1066
1066
  },
1067
1067
  },
@@ -1073,7 +1073,7 @@ export const schemaWithReduceAccumulator = {
1073
1073
  '*': [
1074
1074
  { var: 'working_hours_per_day' },
1075
1075
  {
1076
- reduce: [{ var: 'work_days' }, { '+': [{ var: ['accumulator', 0] }, 1] }, 0],
1076
+ reduce: [{ var: 'work_days' }, { '+': [{ var: 'accumulator' }, 1] }, 0],
1077
1077
  },
1078
1078
  ],
1079
1079
  },
@@ -1081,3 +1081,51 @@ export const schemaWithReduceAccumulator = {
1081
1081
  },
1082
1082
  },
1083
1083
  };
1084
+
1085
+ export const schemaWithReduceAccumulatorAndMerge = {
1086
+ properties: {
1087
+ work_days: {
1088
+ items: {
1089
+ anyOf: [
1090
+ { const: 'monday', title: 'Monday' },
1091
+ { const: 'tuesday', title: 'Tuesday' },
1092
+ { const: 'wednesday', title: 'Wednesday' },
1093
+ { const: 'thursday', title: 'Thursday' },
1094
+ { const: 'friday', title: 'Friday' },
1095
+ { const: 'saturday', title: 'Saturday' },
1096
+ { const: 'sunday', title: 'Sunday' },
1097
+ ],
1098
+ },
1099
+ type: 'array',
1100
+ uniqueItems: true,
1101
+ 'x-jsf-presentation': {
1102
+ inputType: 'select',
1103
+ },
1104
+ },
1105
+ working_hours_per_day: {
1106
+ type: 'number',
1107
+ },
1108
+ working_hours_per_week: {
1109
+ type: 'number',
1110
+ 'x-jsf-logic-computedAttrs': {
1111
+ const: 'computed_work_hours_per_week',
1112
+ default: 'computed_work_hours_per_week',
1113
+ title: '{{computed_work_hours_per_week}} hours per week',
1114
+ },
1115
+ },
1116
+ },
1117
+ 'x-jsf-logic': {
1118
+ computedValues: {
1119
+ computed_work_hours_per_week: {
1120
+ rule: {
1121
+ reduce: [
1122
+ { var: 'work_days' },
1123
+ { merge: [{ var: 'accumulator' }, [{ var: 'current' }]] },
1124
+ // note we use the wrong accumulator type
1125
+ 0,
1126
+ ],
1127
+ },
1128
+ },
1129
+ },
1130
+ },
1131
+ };
@@ -35,6 +35,7 @@ import {
35
35
  schemaWithValidationThatDoesNotExistOnProperty,
36
36
  badSchemaThatWillNotSetAForcedValue,
37
37
  schemaWithReduceAccumulator,
38
+ schemaWithReduceAccumulatorAndMerge,
38
39
  schemaWithComputedPresentationAttributes,
39
40
  } from './jsonLogic.fixtures';
40
41
  import { mockConsole, restoreConsoleAndEnsureItWasNotCalled } from './testUtils';
@@ -245,8 +246,7 @@ describe('jsonLogic: cross-values validations', () => {
245
246
  });
246
247
  });
247
248
 
248
- // TODO: Implement this test.
249
- describe.skip('Reduce', () => {
249
+ describe('Reduce', () => {
250
250
  it('reduce: working_hours_per_day * work_days', () => {
251
251
  const { fields, handleValidation } = createHeadlessForm(schemaWithReduceAccumulator, {
252
252
  strictInputType: false,
@@ -260,6 +260,18 @@ describe('jsonLogic: cross-values validations', () => {
260
260
  expect(field.default).toEqual(16);
261
261
  expect(field.label).toEqual('16 hours per week');
262
262
  });
263
+
264
+ it('reduce: handles when operator is non numerical', () => {
265
+ const { fields, handleValidation } = createHeadlessForm(schemaWithReduceAccumulatorAndMerge, {
266
+ strictInputType: false,
267
+ });
268
+ handleValidation({
269
+ work_days: ['monday', 'tuesday'],
270
+ working_hours_per_day: 8,
271
+ });
272
+ const field = fields.find((i) => i.name === 'working_hours_per_week');
273
+ expect(field.const).toEqual([0, 'monday', 'tuesday']);
274
+ });
263
275
  });
264
276
 
265
277
  describe('Logical: ||, &&', () => {
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 Remote Technology, Inc.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/README.md DELETED
@@ -1,44 +0,0 @@
1
- <p align="center">
2
- <img src=".github/media/jsf_logo_dark.png" width="600" alt="json-schema-form">
3
- </p>
4
-
5
- <p align="center">
6
- <code>json-schema-form</code> is a headless UI form library powered by <a href="https://json-schema.org/">JSON Schemas</a>.
7
- <br/>
8
- It transforms JSON schemas into Javascript `fields` to be more easily consumed by your UI libraries.
9
- </p>
10
-
11
- ---
12
-
13
- ### Why JSON Schemas for forms?
14
-
15
- JSON Schemas are the SSoT (Single Source of Truth) that allows you to share form's data _structure_ and _validations_ between the server (backend) and the client (frontend), regardless of the language used.
16
-
17
- You can use it beyond UI Forms, like lists, tables, and any other UI that needs structured JSON data.
18
-
19
- ## Installation
20
-
21
- Available on [NPM](https://www.npmjs.com/package/@remoteoss/json-schema-form).
22
-
23
- ```bash
24
- npm install @remoteoss/json-schema-form
25
- # or
26
-
27
- yarn install @remoteoss/json-schema-form
28
- ```
29
-
30
- ## Getting Started
31
-
32
- Check the 📚 **[JSF website](https://json-schema-form.vercel.app/)** for documentation.
33
-
34
- ### Playground
35
-
36
- Check the 🕹️ **[JSF Playground](https://json-schema-form.vercel.app/?path=/docs/playground--docs)** for demos.
37
-
38
- ## Contributing
39
-
40
- Read [CONTRIBUTING](CONTRIBUTING.md) to get started.
41
-
42
- We are working on the [next version v1.0](/next). A rewrite in TypeScript with major bugfixes and missing features. It aims to support the latest JSON Schema dialect 2020-12.
43
-
44
- _Backed by [Remote.com](https://remote.com/)_
@@ -1,188 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "JSON json-schema-form-Schema",
4
- "definitions": {
5
- "schemaArray": {
6
- "allOf": [
7
- {
8
- "$ref": "http://json-schema.org/draft-07/schema#/definitions/schemaArray"
9
- },
10
- {
11
- "items": { "$ref": "#" }
12
- }
13
- ]
14
- }
15
- },
16
- "allOf": [{ "$ref": "http://json-schema.org/draft-07/schema#" }],
17
- "properties": {
18
- "additionalItems": { "$ref": "#" },
19
- "additionalProperties": { "$ref": "#" },
20
- "dependencies": {
21
- "additionalProperties": {
22
- "anyOf": [{ "$ref": "#" }, { "type": "array" }]
23
- }
24
- },
25
- "items": {
26
- "anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/schemaArray" }]
27
- },
28
- "definitions": {
29
- "additionalProperties": { "$ref": "#" }
30
- },
31
- "patternProperties": {
32
- "additionalProperties": { "$ref": "#" }
33
- },
34
- "properties": {
35
- "additionalProperties": { "$ref": "#" }
36
- },
37
- "if": { "$ref": "#" },
38
- "then": { "$ref": "#" },
39
- "else": { "$ref": "#" },
40
- "allOf": { "$ref": "#/definitions/schemaArray" },
41
- "anyOf": { "$ref": "#/definitions/schemaArray" },
42
- "oneOf": { "$ref": "#/definitions/schemaArray" },
43
- "not": { "$ref": "#" },
44
- "contains": { "$ref": "#" },
45
- "propertyNames": { "$ref": "#" },
46
- "x-jsf-order": {
47
- "description": "This keyword defines the order of fields for a given form or fieldset. It's placed at the schema root and inside of each fieldset.",
48
- "$ref": "http://json-schema.org/draft-07/schema#/definitions/stringArray"
49
- },
50
- "x-jsf-errorMessage": {
51
- "type": "object",
52
- "additionalProperties": false,
53
- "properties": {
54
- "type": {
55
- "type": "string",
56
- "description": "Message shown when the value is not of the correct type."
57
- },
58
- "required": {
59
- "type": "string",
60
- "description": "Message shown when the value is required and not provided."
61
- },
62
- "minimum": {
63
- "type": "string",
64
- "description": "Message shown when the value is less than the minimum value."
65
- },
66
- "minLength": {
67
- "type": "string",
68
- "description": "Message shown when the value is less than the minimum length."
69
- },
70
- "maximum": {
71
- "type": "string",
72
- "description": "Message shown when the value is greater than the maximum value."
73
- },
74
- "maxLength": {
75
- "type": "string",
76
- "description": "Message shown when the value is greater than the maximum length."
77
- },
78
- "pattern": {
79
- "type": "string",
80
- "description": "Message shown when the value is not the correct format and does not match the pattern property."
81
- },
82
- "maxFileSize": {
83
- "type": "string",
84
- "description": "Message shown when the file size is greater than the maximum file size."
85
- },
86
- "accept": {
87
- "type": "string",
88
- "description": "Message shown when the file type is not accepted."
89
- }
90
- }
91
- },
92
- "x-jsf-presentation": {
93
- "type": "object",
94
- "description": "Presentation overrides for the schema",
95
- "properties": {
96
- "inputType": {
97
- "description": "Input type for the generated UI field",
98
- "type": "string",
99
- "anyOf": [
100
- {
101
- "const": "text",
102
- "description": "Similar to the native HTML input with text type."
103
- },
104
- {
105
- "const": "select",
106
- "description": "Similar to the native HTML select element."
107
- },
108
- {
109
- "const": "radio",
110
- "description": "Similar to a native HTML input with radio type."
111
- },
112
- {
113
- "const": "number",
114
- "description": "Similar to the native HTML input with number type."
115
- },
116
- {
117
- "const": "date",
118
- "description": "Expects a value with format YYY-MM-DD."
119
- },
120
- {
121
- "const": "checkbox",
122
- "description": "Similar to the native HTML input with checkbox type."
123
- },
124
- {
125
- "const": "email",
126
- "description": "Similar to the native HTML input with email type."
127
- },
128
- {
129
- "const": "file",
130
- "description": "Similar to the native HTML input with file type."
131
- },
132
- {
133
- "const": "fieldset",
134
- "description": "Groups multiple Fields inside. Its expected value is a nested object."
135
- },
136
- {
137
- "const": "group-array",
138
- "description": "A list of inputs that can be repeated. Its expected value is an array."
139
- },
140
- {
141
- "type": "string",
142
- "description": "Any arbitrary custom inputType you might want to represent your UI Field."
143
- }
144
- ]
145
- },
146
- "accept": {
147
- "description": "For `inputType: \"file\"`. The accepted file types, as a comma separated string.",
148
- "type": "string"
149
- },
150
- "description": {
151
- "description": "Field description with HTML. If you don't need HTML, please use the native description keyword.",
152
- "type": "string"
153
- },
154
- "statement": {
155
- "description": "Special message about the field. Useful in cases where this message is based on the field value.",
156
- "type": "object",
157
- "properties": {
158
- "title": {
159
- "type": "string"
160
- },
161
- "description": {
162
- "description": "The sentence itself. Might include HTML.",
163
- "type": "string"
164
- },
165
- "severity": {
166
- "description": "Defines the type of message.",
167
- "anyOf": [
168
- { "type": "string", "const": "info" },
169
- { "type": "string", "const": "warning" },
170
- { "type": "string", "const": "error" },
171
- { "type": "string", "const": "success" },
172
- {}
173
- ]
174
- }
175
- }
176
- },
177
- "maxFileSize": {
178
- "description": "For `inputType: \"file\"`. The maximum file size in KB. It's used to enhanced Yup validation.",
179
- "type": "number"
180
- },
181
- "addFieldText": {
182
- "description": "Used in `group-array` fields. The button text to add a new field.",
183
- "type": "string"
184
- }
185
- }
186
- }
187
- }
188
- }
File without changes