hs-uix 1.6.4 → 1.7.0

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/form.mjs CHANGED
@@ -42,6 +42,9 @@ import {
42
42
  CrmPropertyList,
43
43
  CrmAssociationPropertyList
44
44
  } from "@hubspot/ui-extensions/crm";
45
+
46
+ // packages/form/src/formValues.js
47
+ var isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
45
48
  var getEmptyValue = (field) => {
46
49
  switch (field.type) {
47
50
  case "toggle":
@@ -77,12 +80,6 @@ var isValueEmpty = (value, field) => {
77
80
  if ((field.type === "toggle" || field.type === "checkbox") && value === false) return true;
78
81
  return false;
79
82
  };
80
- var isPromise = (value) => value && typeof value.then === "function";
81
- var isAsyncFunction = (fn) => fn && fn.constructor && fn.constructor.name === "AsyncFunction";
82
- var normalizeValidatorResult = (result) => {
83
- if (result === true || result === void 0 || result === null || result === false) return null;
84
- return String(result);
85
- };
86
83
  var isDateValueObject = (value) => isPlainObject(value) && Number.isInteger(value.year) && Number.isInteger(value.month) && Number.isInteger(value.date);
87
84
  var isTimeValueObject = (value) => isPlainObject(value) && Number.isInteger(value.hours) && Number.isInteger(value.minutes);
88
85
  var compareDateValues = (a, b) => {
@@ -94,6 +91,64 @@ var compareTimeValues = (a, b) => {
94
91
  if (a.hours !== b.hours) return a.hours - b.hours;
95
92
  return a.minutes - b.minutes;
96
93
  };
94
+ var deepEqual = (a, b) => {
95
+ if (Object.is(a, b)) return true;
96
+ if (typeof a !== typeof b) return false;
97
+ if (a == null || b == null) return false;
98
+ if (Array.isArray(a)) {
99
+ if (!Array.isArray(b) || a.length !== b.length) return false;
100
+ for (let i = 0; i < a.length; i++) {
101
+ if (!deepEqual(a[i], b[i])) return false;
102
+ }
103
+ return true;
104
+ }
105
+ if (a instanceof Date && b instanceof Date) {
106
+ return a.getTime() === b.getTime();
107
+ }
108
+ if (isPlainObject(a) && isPlainObject(b)) {
109
+ const aKeys = Object.keys(a);
110
+ const bKeys = Object.keys(b);
111
+ if (aKeys.length !== bKeys.length) return false;
112
+ for (const key of aKeys) {
113
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
114
+ if (!deepEqual(a[key], b[key])) return false;
115
+ }
116
+ return true;
117
+ }
118
+ return false;
119
+ };
120
+ var deepClone = (value) => {
121
+ if (Array.isArray(value)) return value.map(deepClone);
122
+ if (value instanceof Date) return new Date(value.getTime());
123
+ if (isPlainObject(value)) {
124
+ const next = {};
125
+ for (const key of Object.keys(value)) {
126
+ next[key] = deepClone(value[key]);
127
+ }
128
+ return next;
129
+ }
130
+ return value;
131
+ };
132
+
133
+ // packages/form/src/formValidation.js
134
+ var isPromise = (value) => value && typeof value.then === "function";
135
+ var isAsyncFunction = (fn) => fn && fn.constructor && fn.constructor.name === "AsyncFunction";
136
+ var normalizeValidatorResult = (result) => {
137
+ if (result === true || result === void 0 || result === null || result === false) return null;
138
+ return String(result);
139
+ };
140
+ var resolveRequired = (field, allValues) => {
141
+ if (typeof field.required === "function") return field.required(allValues);
142
+ return !!field.required;
143
+ };
144
+ var resolveDisabled = (field, allValues) => {
145
+ if (typeof field.disabled === "function") return field.disabled(allValues);
146
+ return !!field.disabled;
147
+ };
148
+ var resolveOptions = (field, allValues) => {
149
+ if (typeof field.options === "function") return field.options(allValues);
150
+ return field.options || [];
151
+ };
97
152
  var runDefaultFieldValidator = (value, field, allValues) => {
98
153
  const errorPrefix = field.label || field.name;
99
154
  switch (field.type) {
@@ -263,50 +318,53 @@ var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
263
318
  }
264
319
  return null;
265
320
  };
266
- var resolveRequired = (field, allValues) => {
267
- if (typeof field.required === "function") return field.required(allValues);
268
- return !!field.required;
269
- };
270
- var resolveDisabled = (field, allValues) => {
271
- if (typeof field.disabled === "function") return field.disabled(allValues);
272
- return !!field.disabled;
273
- };
274
- var resolveOptions = (field, allValues) => {
275
- if (typeof field.options === "function") return field.options(allValues);
276
- return field.options || [];
277
- };
321
+
322
+ // packages/form/src/formDependencies.js
278
323
  var getDependsOnName = (field) => field.dependsOnConfig && field.dependsOnConfig.field;
279
324
  var getDependsOnDisplay = (field) => field.dependsOnConfig && field.dependsOnConfig.display || "grouped";
280
325
  var getDependsOnLabel = (field) => field.dependsOnConfig && field.dependsOnConfig.label;
281
326
  var getDependsOnMessage = (field) => field.dependsOnConfig && field.dependsOnConfig.message;
282
- var getRepeaterErrorKey = (fieldName, rowIdx, subFieldName) => `${fieldName}[${rowIdx}].${subFieldName}`;
283
- var isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
284
- var deepEqual = (a, b) => {
285
- if (Object.is(a, b)) return true;
286
- if (typeof a !== typeof b) return false;
287
- if (a == null || b == null) return false;
288
- if (Array.isArray(a)) {
289
- if (!Array.isArray(b) || a.length !== b.length) return false;
290
- for (let i = 0; i < a.length; i++) {
291
- if (!deepEqual(a[i], b[i])) return false;
292
- }
293
- return true;
294
- }
295
- if (a instanceof Date && b instanceof Date) {
296
- return a.getTime() === b.getTime();
297
- }
298
- if (isPlainObject(a) && isPlainObject(b)) {
299
- const aKeys = Object.keys(a);
300
- const bKeys = Object.keys(b);
301
- if (aKeys.length !== bKeys.length) return false;
302
- for (const key of aKeys) {
303
- if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
304
- if (!deepEqual(a[key], b[key])) return false;
305
- }
306
- return true;
327
+ var resolveDependentCascade = ({ name, value, fields, values, getEmptyValueForField }) => {
328
+ const newValues = { ...values, [name]: value };
329
+ const queue = [name];
330
+ const visited = /* @__PURE__ */ new Set();
331
+ const changedDependents = [];
332
+ while (queue.length > 0) {
333
+ const current = queue.shift();
334
+ if (!current || visited.has(current)) continue;
335
+ visited.add(current);
336
+ fields.forEach((dep) => {
337
+ const parentName = getDependsOnName(dep);
338
+ if (parentName !== current || dep.name === current) return;
339
+ if (!dep.options) return;
340
+ const depOptions = resolveOptions(dep, newValues);
341
+ const depValue = newValues[dep.name];
342
+ if (depValue == null || depValue === "") return;
343
+ const validValues = new Set(depOptions.map((o) => o.value));
344
+ let nextDepValue = depValue;
345
+ let changed = false;
346
+ if (Array.isArray(depValue)) {
347
+ const filtered = depValue.filter((v) => validValues.has(v));
348
+ if (filtered.length !== depValue.length) {
349
+ nextDepValue = filtered;
350
+ changed = true;
351
+ }
352
+ } else if (!validValues.has(depValue)) {
353
+ nextDepValue = getEmptyValueForField(dep);
354
+ changed = true;
355
+ }
356
+ if (changed) {
357
+ newValues[dep.name] = nextDepValue;
358
+ queue.push(dep.name);
359
+ changedDependents.push(dep.name);
360
+ }
361
+ });
307
362
  }
308
- return false;
363
+ return { newValues, changedDependents };
309
364
  };
365
+
366
+ // packages/form/src/FormBuilder.jsx
367
+ var getRepeaterErrorKey = (fieldName, rowIdx, subFieldName) => `${fieldName}[${rowIdx}].${subFieldName}`;
310
368
  var fieldSetHasErrors = (errors, fields) => {
311
369
  if (!errors || !fields || fields.length === 0) return false;
312
370
  const names = new Set(fields.map((field) => field.name));
@@ -315,18 +373,6 @@ var fieldSetHasErrors = (errors, fields) => {
315
373
  return names.has(base);
316
374
  });
317
375
  };
318
- var deepClone = (value) => {
319
- if (Array.isArray(value)) return value.map(deepClone);
320
- if (value instanceof Date) return new Date(value.getTime());
321
- if (isPlainObject(value)) {
322
- const next = {};
323
- for (const key of Object.keys(value)) {
324
- next[key] = deepClone(value[key]);
325
- }
326
- return next;
327
- }
328
- return value;
329
- };
330
376
  var useFormPrefill = (properties, mapping) => {
331
377
  return useMemo(() => {
332
378
  if (!properties) return {};
@@ -382,8 +428,12 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
382
428
  // validate on blur
383
429
  validateOnSubmit = true,
384
430
  // validate all before onSubmit
385
- onValidationChange
431
+ onValidationChange,
386
432
  // (errors) => void
433
+ onValidationFail,
434
+ // ({ errors, fields, firstInvalidField }) => void — called when submit-time validation blocks submission
435
+ openSectionOnValidationFail = false
436
+ // auto-open accordion section containing first invalid field on submit failure
387
437
  } = props;
388
438
  const {
389
439
  steps,
@@ -532,6 +582,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
532
582
  "tooltip",
533
583
  "required",
534
584
  "readOnly",
585
+ "alwaysEditable",
535
586
  "disabled",
536
587
  "defaultValue",
537
588
  "fieldProps",
@@ -704,6 +755,17 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
704
755
  }
705
756
  return map;
706
757
  }, [fields]);
758
+ const sectionIdByFieldName = useMemo(() => {
759
+ const map = /* @__PURE__ */ new Map();
760
+ if (Array.isArray(sections)) {
761
+ for (const sec of sections) {
762
+ if (!sec || !Array.isArray(sec.fields)) continue;
763
+ for (const name of sec.fields) map.set(name, sec.id);
764
+ }
765
+ }
766
+ return map;
767
+ }, [sections]);
768
+ const [validationOpenSection, setValidationOpenSection] = useState(null);
707
769
  const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
708
770
  const configWarningsRef = useRef(/* @__PURE__ */ new Set());
709
771
  const warnConfig = useCallback((message) => {
@@ -1177,42 +1239,18 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1177
1239
  const handleFieldChange = useCallback(
1178
1240
  (name, value, options = {}) => {
1179
1241
  const { clearNestedErrors = true } = options;
1180
- const newValues = { ...formValuesRef.current, [name]: value };
1181
- const queue = [name];
1182
- const visited = /* @__PURE__ */ new Set();
1242
+ const { newValues, changedDependents } = resolveDependentCascade({
1243
+ name,
1244
+ value,
1245
+ fields,
1246
+ values: formValuesRef.current,
1247
+ getEmptyValueForField: getFieldEmptyValue
1248
+ });
1183
1249
  const clearedErrors = {};
1184
- while (queue.length > 0) {
1185
- const current = queue.shift();
1186
- if (!current || visited.has(current)) continue;
1187
- visited.add(current);
1188
- fields.forEach((dep) => {
1189
- const parentName = getDependsOnName(dep);
1190
- if (parentName !== current || dep.name === current) return;
1191
- if (!dep.options) return;
1192
- const depOptions = resolveOptions(dep, newValues);
1193
- const depValue = newValues[dep.name];
1194
- if (depValue == null || depValue === "") return;
1195
- const validValues = new Set(depOptions.map((o) => o.value));
1196
- let nextDepValue = depValue;
1197
- let changed = false;
1198
- if (Array.isArray(depValue)) {
1199
- const filtered = depValue.filter((v) => validValues.has(v));
1200
- if (filtered.length !== depValue.length) {
1201
- nextDepValue = filtered;
1202
- changed = true;
1203
- }
1204
- } else if (!validValues.has(depValue)) {
1205
- nextDepValue = getFieldEmptyValue(dep);
1206
- changed = true;
1207
- }
1208
- if (changed) {
1209
- newValues[dep.name] = nextDepValue;
1210
- queue.push(dep.name);
1211
- if (formErrorsRef.current[dep.name] != null) {
1212
- clearedErrors[dep.name] = null;
1213
- }
1214
- }
1215
- });
1250
+ for (const depName of changedDependents) {
1251
+ if (formErrorsRef.current[depName] != null) {
1252
+ clearedErrors[depName] = null;
1253
+ }
1216
1254
  }
1217
1255
  if (formErrorsRef.current[name] != null) {
1218
1256
  clearedErrors[name] = null;
@@ -1284,10 +1322,35 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1284
1322
  const handleSubmit = useCallback(
1285
1323
  async (e) => {
1286
1324
  if (e && e.preventDefault) e.preventDefault();
1325
+ const reportValidationFailure = (errors) => {
1326
+ const errorNames = Object.keys(errors).filter((n) => !!errors[n]);
1327
+ if (errorNames.length === 0) return;
1328
+ const orderedNames = allVisibleFields.map((f) => f.name).filter((n) => errorNames.includes(n));
1329
+ for (const n of errorNames) if (!orderedNames.includes(n)) orderedNames.push(n);
1330
+ const fieldInfos = orderedNames.map((name) => {
1331
+ const f = fieldByName.get(name);
1332
+ return {
1333
+ name,
1334
+ label: f == null ? void 0 : f.label,
1335
+ sectionId: sectionIdByFieldName.get(name)
1336
+ };
1337
+ });
1338
+ const firstInvalidField = fieldInfos[0];
1339
+ if (openSectionOnValidationFail && (firstInvalidField == null ? void 0 : firstInvalidField.sectionId)) {
1340
+ setValidationOpenSection({
1341
+ id: firstInvalidField.sectionId,
1342
+ nonce: ((validationOpenSection == null ? void 0 : validationOpenSection.nonce) || 0) + 1
1343
+ });
1344
+ }
1345
+ if (onValidationFail) {
1346
+ onValidationFail({ errors, fields: fieldInfos, firstInvalidField });
1347
+ }
1348
+ };
1287
1349
  if (validateOnSubmit) {
1288
1350
  const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
1289
1351
  if (hasErrors) {
1290
1352
  replaceErrors(errors);
1353
+ reportValidationFailure(errors);
1291
1354
  return;
1292
1355
  }
1293
1356
  const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
@@ -1299,7 +1362,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1299
1362
  ])
1300
1363
  ];
1301
1364
  await Promise.all(pendingValidations);
1302
- if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
1365
+ if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) {
1366
+ reportValidationFailure(formErrorsRef.current);
1367
+ return;
1368
+ }
1303
1369
  }
1304
1370
  }
1305
1371
  const reset = () => {
@@ -1345,7 +1411,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1345
1411
  if (controlledLoading == null) setInternalLoading(false);
1346
1412
  }
1347
1413
  },
1348
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
1414
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget, onValidationFail, openSectionOnValidationFail, sectionIdByFieldName, validationOpenSection]
1349
1415
  );
1350
1416
  const handleNext = useCallback(async () => {
1351
1417
  if (!isMultiStep) return;
@@ -1438,8 +1504,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1438
1504
  const fieldError = formErrors[field.name] || null;
1439
1505
  const hasError = !!fieldError;
1440
1506
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
1441
- const isReadOnly = field.readOnly || formReadOnly;
1442
- const isDisabled = disabled || resolveDisabled(field, formValues) || formReadOnly;
1507
+ const fieldFormReadOnly = field.alwaysEditable ? false : formReadOnly;
1508
+ const isReadOnly = field.readOnly || fieldFormReadOnly;
1509
+ const isDisabled = disabled || resolveDisabled(field, formValues) || fieldFormReadOnly;
1443
1510
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
1444
1511
  if (field.type === "display" || field.type === "slot") {
1445
1512
  if (field.render) {
@@ -1482,8 +1549,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1482
1549
  const sfValue = formValues[sf.name];
1483
1550
  const sfError = formErrors[sf.name] || null;
1484
1551
  const sfLabel = itemIdx === 0 ? sf.label : void 0;
1485
- const sfReadOnly = sf.readOnly || formReadOnly;
1486
- const sfDisabled = disabled || resolveDisabled(sf, formValues) || formReadOnly;
1552
+ const sfFormReadOnly = sf.alwaysEditable ? false : formReadOnly;
1553
+ const sfReadOnly = sf.readOnly || sfFormReadOnly;
1554
+ const sfDisabled = disabled || resolveDisabled(sf, formValues) || sfFormReadOnly;
1487
1555
  const sfOnChange = sf.debounce ? (v) => handleDebouncedFieldChange(sf.name, v) : (v) => handleFieldChange(sf.name, v);
1488
1556
  const sfProps = {
1489
1557
  name: sf.name,
@@ -2000,7 +2068,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2000
2068
  );
2001
2069
  }
2002
2070
  return /* @__PURE__ */ React.createElement(Box, { key: sf.name, flex: 1 }, sfElement);
2003
- }), /* @__PURE__ */ React.createElement(Inline, { gap: "xs" }, canReorder && rowIdx > 0 && (renderMoveUpControl ? renderMoveUpControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx - 1) }) : /* @__PURE__ */ React.createElement(Button, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx - 1) }, moveUpLabel)), canReorder && rowIdx < rows.length - 1 && (renderMoveDownControl ? renderMoveDownControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx + 1) }) : /* @__PURE__ */ React.createElement(Button, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx + 1) }, moveDownLabel)), canRemove && (renderRemoveControl ? renderRemoveControl({ index: rowIdx, onClick: () => removeRow(rowIdx) }) : /* @__PURE__ */ React.createElement(
2071
+ }), /* @__PURE__ */ React.createElement(Inline, { gap: "xs" }, canReorder && (renderMoveUpControl ? renderMoveUpControl({ index: rowIdx, disabled: rowIdx === 0, onClick: () => moveRow(rowIdx, rowIdx - 1) }) : /* @__PURE__ */ React.createElement(Button, { variant: "secondary", size: "sm", disabled: rowIdx === 0, onClick: () => moveRow(rowIdx, rowIdx - 1) }, moveUpLabel)), canReorder && (renderMoveDownControl ? renderMoveDownControl({ index: rowIdx, disabled: rowIdx === rows.length - 1, onClick: () => moveRow(rowIdx, rowIdx + 1) }) : /* @__PURE__ */ React.createElement(Button, { variant: "secondary", size: "sm", disabled: rowIdx === rows.length - 1, onClick: () => moveRow(rowIdx, rowIdx + 1) }, moveDownLabel)), canRemove && (renderRemoveControl ? renderRemoveControl({ index: rowIdx, onClick: () => removeRow(rowIdx) }) : /* @__PURE__ */ React.createElement(
2004
2072
  Button,
2005
2073
  {
2006
2074
  variant: "secondary",
@@ -2008,7 +2076,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2008
2076
  onClick: () => removeRow(rowIdx)
2009
2077
  },
2010
2078
  removeLabel
2011
- ))))), canAdd && (renderAddControl ? renderAddControl({ onClick: addRow, count: rows.length }) : /* @__PURE__ */ React.createElement(Link, { onClick: addRow }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "flush" }, /* @__PURE__ */ React.createElement(Icon, { name: "add" }), /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, addLabel)))), repeaterHasError && repeaterErrorMessage && /* @__PURE__ */ React.createElement(Text, { variant: "microcopy" }, repeaterErrorMessage));
2079
+ ))))), canAdd && (renderAddControl ? renderAddControl({ onClick: addRow, count: rows.length }) : /* @__PURE__ */ React.createElement(Link, { onClick: addRow }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React.createElement(Icon, { name: "add" }), /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, addLabel)))), repeaterHasError && repeaterErrorMessage && /* @__PURE__ */ React.createElement(Text, { variant: "microcopy" }, repeaterErrorMessage));
2012
2080
  }
2013
2081
  default:
2014
2082
  return /* @__PURE__ */ React.createElement(
@@ -2280,13 +2348,16 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2280
2348
  const sectionContext = { values: formValues, errors: formErrors };
2281
2349
  const sectionOverrides = sec.columns ? { columns: sec.columns } : void 0;
2282
2350
  const accordionContent = /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap }, sec.renderBefore && sec.renderBefore(sectionContext), renderFieldSubset(sectionFields, sectionOverrides), sec.renderAfter && sec.renderAfter(sectionContext));
2351
+ const isValidationOverrideTarget = validationOpenSection && validationOpenSection.id === sec.id;
2352
+ const accordionKey = isValidationOverrideTarget ? `${sec.id}::open::${validationOpenSection.nonce}` : sec.id;
2353
+ const accordionDefaultOpen = isValidationOverrideTarget ? true : sec.defaultOpen !== false;
2283
2354
  const accordion = /* @__PURE__ */ React.createElement(
2284
2355
  Accordion,
2285
2356
  {
2286
- key: sec.id,
2357
+ key: accordionKey,
2287
2358
  title: sec.label,
2288
2359
  size: "sm",
2289
- defaultOpen: sec.defaultOpen !== false
2360
+ defaultOpen: accordionDefaultOpen
2290
2361
  },
2291
2362
  accordionContent
2292
2363
  );