hs-uix 2.1.0 → 2.2.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.
Files changed (49) hide show
  1. package/README.md +3 -1
  2. package/common-components.d.ts +319 -68
  3. package/dist/calendar.js +397 -119
  4. package/dist/calendar.mjs +399 -119
  5. package/dist/common-components.js +3546 -88
  6. package/dist/common-components.mjs +3530 -84
  7. package/dist/datatable.js +108 -18
  8. package/dist/datatable.mjs +108 -18
  9. package/dist/experimental.js +2876 -0
  10. package/dist/experimental.mjs +2883 -0
  11. package/dist/feed.js +267 -38
  12. package/dist/feed.mjs +260 -37
  13. package/dist/filter.js +1379 -0
  14. package/dist/filter.mjs +1334 -0
  15. package/dist/form.js +222 -26
  16. package/dist/form.mjs +227 -27
  17. package/dist/index.js +3255 -353
  18. package/dist/index.mjs +3199 -344
  19. package/dist/kanban.js +282 -62
  20. package/dist/kanban.mjs +273 -61
  21. package/dist/safe.js +9207 -0
  22. package/dist/safe.mjs +9298 -0
  23. package/dist/utils.js +491 -75
  24. package/dist/utils.mjs +491 -75
  25. package/experimental.d.ts +1 -0
  26. package/filter.d.ts +1 -0
  27. package/index.d.ts +45 -3
  28. package/package.json +19 -1
  29. package/safe.d.ts +1 -0
  30. package/src/calendar/README.md +76 -5
  31. package/src/calendar/index.d.ts +108 -1
  32. package/src/common-components/README.md +140 -1
  33. package/src/datatable/README.md +0 -2
  34. package/src/experimental/README.md +126 -0
  35. package/src/experimental/index.d.ts +346 -0
  36. package/src/feed/README.md +69 -0
  37. package/src/feed/index.d.ts +103 -0
  38. package/src/filter/README.md +148 -0
  39. package/src/filter/index.d.ts +221 -0
  40. package/src/form/README.md +132 -4
  41. package/src/form/index.d.ts +82 -1
  42. package/src/kanban/README.md +119 -6
  43. package/src/kanban/index.d.ts +153 -2
  44. package/src/safe/README.md +108 -0
  45. package/src/safe/index.d.ts +158 -0
  46. package/src/utils/README.md +39 -0
  47. package/src/wizard/README.md +158 -0
  48. package/src/wizard/index.d.ts +138 -0
  49. package/utils.d.ts +17 -0
package/dist/form.mjs CHANGED
@@ -35,7 +35,12 @@ import {
35
35
  TimeInput,
36
36
  Toggle,
37
37
  Checkbox,
38
- ToggleGroup
38
+ ToggleGroup,
39
+ Modal,
40
+ ModalBody,
41
+ ModalFooter,
42
+ LoadingSpinner,
43
+ useExtensionActions
39
44
  } from "@hubspot/ui-extensions";
40
45
 
41
46
  // src/common-components/Icon.js
@@ -297,7 +302,7 @@ var GENERATED_ICONS = {
297
302
  "ZoomOut": { "viewBox": "0 0 32 32", "paths": ["M14.42 26.75c2.85 0 5.47-.97 7.56-2.6l-.03.02 5.28 5.34a1.619 1.619 0 0 0 2.76-1.15c0-.45-.18-.85-.47-1.14l-5.33-5.33c1.59-2.06 2.55-4.68 2.55-7.52C26.74 7.54 21.2 2 14.37 2S2 7.55 2 14.38s5.54 12.37 12.37 12.37h.05m0-21.55c5.06 0 9.16 4.1 9.16 9.16s-4.1 9.16-9.16 9.16-9.16-4.1-9.16-9.16c.01-5.05 4.11-9.14 9.16-9.15Zm-4.31 10.78h8.62c.89 0 1.62-.72 1.62-1.62s-.72-1.62-1.62-1.62h-8.62c-.89 0-1.62.72-1.62 1.62s.72 1.62 1.62 1.62"] }
298
303
  };
299
304
 
300
- // src/common-components/Icon.js
305
+ // src/common-components/nativeIconNames.js
301
306
  var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
302
307
  "add",
303
308
  "appointment",
@@ -309,12 +314,12 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
309
314
  "block",
310
315
  "book",
311
316
  "bulb",
317
+ "callTranscript",
312
318
  "calling",
313
319
  "callingHangup",
314
320
  "callingMade",
315
321
  "callingMissed",
316
322
  "callingVoicemail",
317
- "callTranscript",
318
323
  "campaigns",
319
324
  "cap",
320
325
  "checkCircle",
@@ -343,13 +348,13 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
343
348
  "enroll",
344
349
  "exclamation",
345
350
  "exclamationCircle",
346
- "facebook",
347
351
  "faceHappy",
348
352
  "faceHappyFilled",
349
353
  "faceNeutral",
350
354
  "faceNeutralFilled",
351
355
  "faceSad",
352
356
  "faceSadFilled",
357
+ "facebook",
353
358
  "favoriteHollow",
354
359
  "file",
355
360
  "filledXCircleIcon",
@@ -490,6 +495,8 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
490
495
  "zoomIn",
491
496
  "zoomOut"
492
497
  ]);
498
+
499
+ // src/common-components/Icon.js
493
500
  var NATIVE_COLORS = /* @__PURE__ */ new Set(["inherit", "alert", "warning", "success"]);
494
501
  var NATIVE_SIZE_TOKENS = {
495
502
  sm: "sm",
@@ -988,8 +995,25 @@ var resolveDependentCascade = ({ name, value, fields, values, getEmptyValueForFi
988
995
  return { newValues, changedDependents };
989
996
  };
990
997
 
998
+ // src/form/formDirty.js
999
+ var isFormDirty = (values, initialValues) => !deepEqual(values || {}, initialValues || {});
1000
+ var getDirtyFields = (values, initialValues) => {
1001
+ const current = values || {};
1002
+ const baseline = initialValues || {};
1003
+ const names = [...Object.keys(current)];
1004
+ for (const name of Object.keys(baseline)) {
1005
+ if (!Object.prototype.hasOwnProperty.call(current, name)) names.push(name);
1006
+ }
1007
+ return names.filter((name) => !deepEqual(current[name], baseline[name]));
1008
+ };
1009
+
991
1010
  // src/form/FormBuilder.jsx
992
1011
  var getRepeaterErrorKey = (fieldName, rowIdx, subFieldName) => `${fieldName}[${rowIdx}].${subFieldName}`;
1012
+ var withFieldLoadingSpinner = (element, loading) => {
1013
+ if (!loading) return element;
1014
+ return /* @__PURE__ */ React2.createElement(Flex, { direction: "row", gap: "xs", align: "end" }, /* @__PURE__ */ React2.createElement(Box, { flex: 1 }, element), /* @__PURE__ */ React2.createElement(LoadingSpinner, { size: "xs", layout: "inline" }));
1015
+ };
1016
+ var formBuilderInstanceCounter = 0;
993
1017
  var fieldSetHasErrors = (errors, fields) => {
994
1018
  if (!errors || !fields || fields.length === 0) return false;
995
1019
  const names = new Set(fields.map((field) => field.name));
@@ -1089,8 +1113,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1089
1113
  // controlled loading state
1090
1114
  disabled = false,
1091
1115
  // disable entire form
1092
- renderButtons: renderButtonsProp
1116
+ renderButtons: renderButtonsProp,
1093
1117
  // custom action row renderer
1118
+ confirmDiscard
1119
+ // true | { title, message, confirmLabel, cancelLabel } — confirm before the built-in Cancel discards dirty changes
1094
1120
  } = props;
1095
1121
  const {
1096
1122
  columns = 1,
@@ -1209,6 +1235,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1209
1235
  "readOnly",
1210
1236
  "alwaysEditable",
1211
1237
  "disabled",
1238
+ "loading",
1212
1239
  "defaultValue",
1213
1240
  "fieldProps",
1214
1241
  "colSpan",
@@ -1465,7 +1492,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1465
1492
  syncDirtyBaseline(values);
1466
1493
  }, [values, syncDirtyBaseline]);
1467
1494
  const isDirty = useMemo(() => {
1468
- return !deepEqual(formValues, initialSnapshot.current);
1495
+ return isFormDirty(formValues, initialSnapshot.current);
1469
1496
  }, [formValues]);
1470
1497
  const prevDirtyRef = useRef(false);
1471
1498
  useEffect(() => {
@@ -1474,6 +1501,27 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1474
1501
  if (onDirtyChange) onDirtyChange(isDirty);
1475
1502
  }
1476
1503
  }, [isDirty, onDirtyChange]);
1504
+ let hostActions = null;
1505
+ try {
1506
+ hostActions = useExtensionActions();
1507
+ } catch (err) {
1508
+ hostActions = null;
1509
+ }
1510
+ const discardModalIdRef = useRef(null);
1511
+ if (discardModalIdRef.current === null) {
1512
+ formBuilderInstanceCounter += 1;
1513
+ discardModalIdRef.current = `hs-uix-form-discard-${formBuilderInstanceCounter}`;
1514
+ }
1515
+ const discardConfig = confirmDiscard ? confirmDiscard === true ? {} : confirmDiscard : null;
1516
+ const discardTitle = discardConfig && discardConfig.title || "Discard changes?";
1517
+ const discardMessage = discardConfig && discardConfig.message || "You have unsaved changes. If you discard now, they will be lost.";
1518
+ const discardConfirmLabel = discardConfig && discardConfig.confirmLabel || "Discard changes";
1519
+ const discardCancelLabel = discardConfig && discardConfig.cancelLabel || "Keep editing";
1520
+ const closeDiscardModal = useCallback(() => {
1521
+ if (hostActions && typeof hostActions.closeOverlay === "function") {
1522
+ hostActions.closeOverlay(discardModalIdRef.current);
1523
+ }
1524
+ }, [hostActions]);
1477
1525
  const autoSaveTimerRef = useRef(null);
1478
1526
  const autoSaveRef = useRef(autoSave);
1479
1527
  autoSaveRef.current = autoSave;
@@ -2112,6 +2160,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2112
2160
  },
2113
2161
  getValues: () => formValues,
2114
2162
  isDirty: () => isDirty,
2163
+ getDirtyFields: () => getDirtyFields(formValuesRef.current, initialSnapshot.current),
2115
2164
  setFieldValue: (name, value) => handleFieldChange(name, value),
2116
2165
  setFieldError: (name, message) => updateErrors({ [name]: message }),
2117
2166
  setErrors: (errors) => {
@@ -2131,7 +2180,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2131
2180
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
2132
2181
  const fieldFormReadOnly = field.alwaysEditable ? false : formReadOnly;
2133
2182
  const isReadOnly = field.readOnly || fieldFormReadOnly;
2134
- const isDisabled = disabled || resolveDisabled(field, formValues) || fieldFormReadOnly;
2183
+ const isFieldLoading = !!field.loading;
2184
+ const isDisabled = disabled || resolveDisabled(field, formValues) || fieldFormReadOnly || isFieldLoading;
2135
2185
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
2136
2186
  if (field.type === "display" || field.type === "slot") {
2137
2187
  if (field.render) {
@@ -2315,7 +2365,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2315
2365
  disabled: isDisabled,
2316
2366
  error: hasError,
2317
2367
  validationMessage: renderFieldError ? void 0 : fieldError || void 0,
2318
- ...field.loading || validatingFields[field.name] ? { loading: true } : {},
2368
+ ...validatingFields[field.name] ? { loading: true } : {},
2319
2369
  ...field.fieldProps || {}
2320
2370
  };
2321
2371
  const options = resolveOptions(field, formValues);
@@ -2473,25 +2523,31 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2473
2523
  )));
2474
2524
  }
2475
2525
  case "select":
2476
- return /* @__PURE__ */ React2.createElement(
2477
- Select,
2478
- {
2479
- ...commonProps,
2480
- value: fieldValue,
2481
- options,
2482
- variant: field.variant,
2483
- onChange: fieldOnChange
2484
- }
2526
+ return withFieldLoadingSpinner(
2527
+ /* @__PURE__ */ React2.createElement(
2528
+ Select,
2529
+ {
2530
+ ...commonProps,
2531
+ value: fieldValue,
2532
+ options,
2533
+ variant: field.variant,
2534
+ onChange: fieldOnChange
2535
+ }
2536
+ ),
2537
+ isFieldLoading
2485
2538
  );
2486
2539
  case "multiselect":
2487
- return /* @__PURE__ */ React2.createElement(
2488
- MultiSelect,
2489
- {
2490
- ...commonProps,
2491
- value: fieldValue || [],
2492
- options,
2493
- onChange: fieldOnChange
2494
- }
2540
+ return withFieldLoadingSpinner(
2541
+ /* @__PURE__ */ React2.createElement(
2542
+ MultiSelect,
2543
+ {
2544
+ ...commonProps,
2545
+ value: fieldValue || [],
2546
+ options,
2547
+ onChange: fieldOnChange
2548
+ }
2549
+ ),
2550
+ isFieldLoading
2495
2551
  );
2496
2552
  case "toggle":
2497
2553
  return /* @__PURE__ */ React2.createElement(
@@ -3012,6 +3068,30 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3012
3068
  if (layout) return renderExplicitLayout();
3013
3069
  return renderFieldSubset(visibleFields);
3014
3070
  };
3071
+ const renderCancelButton = () => {
3072
+ if (discardConfig && isDirty) {
3073
+ return /* @__PURE__ */ React2.createElement(
3074
+ Button,
3075
+ {
3076
+ variant: "secondary",
3077
+ disabled,
3078
+ overlay: /* @__PURE__ */ React2.createElement(Modal, { id: discardModalIdRef.current, title: discardTitle, width: "small" }, /* @__PURE__ */ React2.createElement(ModalBody, null, /* @__PURE__ */ React2.createElement(Text, null, discardMessage)), /* @__PURE__ */ React2.createElement(ModalFooter, null, /* @__PURE__ */ React2.createElement(Button, { variant: "secondary", onClick: closeDiscardModal }, discardCancelLabel), /* @__PURE__ */ React2.createElement(
3079
+ Button,
3080
+ {
3081
+ variant: "destructive",
3082
+ onClick: () => {
3083
+ closeDiscardModal();
3084
+ if (onCancel) onCancel();
3085
+ }
3086
+ },
3087
+ discardConfirmLabel
3088
+ )))
3089
+ },
3090
+ cancelButtonLabel
3091
+ );
3092
+ }
3093
+ return /* @__PURE__ */ React2.createElement(Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel);
3094
+ };
3015
3095
  const renderButtons = () => {
3016
3096
  if (submitPosition === "none" || formReadOnly) return null;
3017
3097
  const isLastStep = !isMultiStep || currentStep === steps.length - 1;
@@ -3025,6 +3105,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3025
3105
  totalSteps: isMultiStep ? steps.length : 1,
3026
3106
  disabled,
3027
3107
  loading: isLoading,
3108
+ isDirty,
3028
3109
  labels: {
3029
3110
  submit: submitButtonLabel,
3030
3111
  cancel: cancelButtonLabel,
@@ -3040,7 +3121,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3040
3121
  return renderButtonsProp(buttonContext);
3041
3122
  }
3042
3123
  if (isMultiStep) {
3043
- return /* @__PURE__ */ React2.createElement(Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ React2.createElement(Button, { variant: "secondary", onClick: handleBack, disabled }, backButtonLabel) : showCancel ? /* @__PURE__ */ React2.createElement(Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel) : /* @__PURE__ */ React2.createElement(Text, null, " "), /* @__PURE__ */ React2.createElement(Inline, { gap: "small" }, /* @__PURE__ */ React2.createElement(Text, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ React2.createElement(
3124
+ return /* @__PURE__ */ React2.createElement(Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ React2.createElement(Button, { variant: "secondary", onClick: handleBack, disabled }, backButtonLabel) : showCancel ? renderCancelButton() : /* @__PURE__ */ React2.createElement(Text, null, " "), /* @__PURE__ */ React2.createElement(Inline, { gap: "small" }, /* @__PURE__ */ React2.createElement(Text, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ React2.createElement(
3044
3125
  LoadingButton,
3045
3126
  {
3046
3127
  variant: submitVariant,
@@ -3051,7 +3132,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3051
3132
  submitButtonLabel
3052
3133
  ) : /* @__PURE__ */ React2.createElement(Button, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
3053
3134
  }
3054
- return /* @__PURE__ */ React2.createElement(Flex, { direction: "row", justify: singleStepJustify, gap: "sm" }, showCancel && /* @__PURE__ */ React2.createElement(Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ React2.createElement(
3135
+ return /* @__PURE__ */ React2.createElement(Flex, { direction: "row", justify: singleStepJustify, gap: "sm" }, showCancel && renderCancelButton(), /* @__PURE__ */ React2.createElement(
3055
3136
  LoadingButton,
3056
3137
  {
3057
3138
  variant: submitVariant,
@@ -3091,7 +3172,126 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3091
3172
  formContent
3092
3173
  );
3093
3174
  });
3175
+ FormBuilder.displayName = "FormBuilder";
3176
+
3177
+ // src/form/hubspotSchema.js
3178
+ var coerceBool = (value) => value === true || value === "true" || value === "Yes" || value === "yes" || value === "1";
3179
+ var coerceNumber = (value) => {
3180
+ if (value === null || value === void 0 || value === "") return void 0;
3181
+ if (typeof value === "number") return value;
3182
+ const parsed = Number(value);
3183
+ return Number.isNaN(parsed) ? void 0 : parsed;
3184
+ };
3185
+ var mapPropertyOptions = (options) => {
3186
+ if (!Array.isArray(options)) return void 0;
3187
+ const mapped = options.filter((opt) => opt && opt.hidden !== true).map((opt) => {
3188
+ const result = { label: opt.label != null ? opt.label : String(opt.value), value: opt.value };
3189
+ if (opt.description != null && opt.description !== "") result.description = opt.description;
3190
+ return result;
3191
+ });
3192
+ return mapped;
3193
+ };
3194
+ var resolveFieldType = (property) => {
3195
+ const { type, fieldType } = property;
3196
+ if (type === "datetime") return "datetime";
3197
+ if (type === "date" || fieldType === "date") return "date";
3198
+ switch (fieldType) {
3199
+ case "select":
3200
+ return "select";
3201
+ // FormBuilder's radio rendering is the "radioGroup" type (native
3202
+ // ToggleGroup with toggleType="radioButtonList").
3203
+ case "radio":
3204
+ return "radioGroup";
3205
+ // HubSpot "checkbox" = multiple checkboxes over an enumeration → multiselect.
3206
+ case "checkbox":
3207
+ return "multiselect";
3208
+ case "booleancheckbox":
3209
+ return "toggle";
3210
+ case "number":
3211
+ return "number";
3212
+ case "textarea":
3213
+ return "textarea";
3214
+ case "text":
3215
+ case "phonenumber":
3216
+ return "text";
3217
+ default:
3218
+ break;
3219
+ }
3220
+ switch (type) {
3221
+ case "enumeration":
3222
+ return "select";
3223
+ case "number":
3224
+ return "number";
3225
+ case "bool":
3226
+ return "toggle";
3227
+ default:
3228
+ return "text";
3229
+ }
3230
+ };
3231
+ var isPropertyReadOnly = (property) => property.calculated === true || property.modificationMetadata && property.modificationMetadata.readOnlyValue === true;
3232
+ var resolveRequiredOverride = (requiredOverrides, name) => {
3233
+ if (!requiredOverrides) return void 0;
3234
+ if (Array.isArray(requiredOverrides)) {
3235
+ return requiredOverrides.includes(name) ? true : void 0;
3236
+ }
3237
+ if (Object.prototype.hasOwnProperty.call(requiredOverrides, name)) {
3238
+ return !!requiredOverrides[name];
3239
+ }
3240
+ return void 0;
3241
+ };
3242
+ var fieldsFromHubSpotProperties = (properties, options = {}) => {
3243
+ if (!Array.isArray(properties)) return [];
3244
+ const {
3245
+ include,
3246
+ exclude,
3247
+ overrides,
3248
+ requiredOverrides,
3249
+ includeDescriptions = false
3250
+ } = options;
3251
+ const includeSet = Array.isArray(include) ? new Set(include) : null;
3252
+ const excludeSet = Array.isArray(exclude) ? new Set(exclude) : null;
3253
+ let selected = properties.filter((property) => {
3254
+ if (!property || !property.name) return false;
3255
+ if (includeSet && !includeSet.has(property.name)) return false;
3256
+ if (excludeSet && excludeSet.has(property.name)) return false;
3257
+ if (property.hidden === true && !includeSet) return false;
3258
+ return true;
3259
+ });
3260
+ if (includeSet) {
3261
+ const order = new Map(include.map((name, idx) => [name, idx]));
3262
+ selected = [...selected].sort((a, b) => order.get(a.name) - order.get(b.name));
3263
+ }
3264
+ return selected.map((property) => {
3265
+ const type = resolveFieldType(property);
3266
+ const field = {
3267
+ name: property.name,
3268
+ type,
3269
+ label: property.label || property.name
3270
+ };
3271
+ if (includeDescriptions && property.description) {
3272
+ field.description = property.description;
3273
+ }
3274
+ if (type === "select" || type === "multiselect" || type === "radioGroup") {
3275
+ field.options = mapPropertyOptions(property.options) || [];
3276
+ }
3277
+ if (type === "toggle") {
3278
+ field.transformIn = coerceBool;
3279
+ field.transformOut = (value) => !!value;
3280
+ }
3281
+ if (type === "number") {
3282
+ field.transformIn = coerceNumber;
3283
+ }
3284
+ if (isPropertyReadOnly(property)) {
3285
+ field.readOnly = true;
3286
+ }
3287
+ const required = resolveRequiredOverride(requiredOverrides, property.name);
3288
+ if (required !== void 0) field.required = required;
3289
+ const override = overrides && overrides[property.name];
3290
+ return override ? { ...field, ...override } : field;
3291
+ });
3292
+ };
3094
3293
  export {
3095
3294
  FormBuilder,
3295
+ fieldsFromHubSpotProperties,
3096
3296
  useFormPrefill
3097
3297
  };