@osdk/react-components 0.3.0 → 0.4.0-main-90a01d202dfa1e497125bd6cfcdc8175278fb7ec

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 (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/build/browser/action-form/ActionFormApi.js.map +1 -1
  3. package/build/browser/action-form/BaseForm.js +100 -12
  4. package/build/browser/action-form/BaseForm.js.map +1 -1
  5. package/build/browser/action-form/BaseForm.module.css +21 -1
  6. package/build/browser/action-form/BaseForm.module.css.js +4 -1
  7. package/build/browser/action-form/FormField.js +3 -1
  8. package/build/browser/action-form/FormField.js.map +1 -1
  9. package/build/browser/action-form/FormFieldApi.js.map +1 -1
  10. package/build/browser/action-form/fields/BaseInput.module.css +4 -0
  11. package/build/browser/action-form/fields/DatetimePickerField.js +3 -1
  12. package/build/browser/action-form/fields/DatetimePickerField.js.map +1 -1
  13. package/build/browser/action-form/fields/DatetimePickerField.module.css +4 -0
  14. package/build/browser/action-form/fields/FieldBridge.js +31 -4
  15. package/build/browser/action-form/fields/FieldBridge.js.map +1 -1
  16. package/build/browser/action-form/fields/FilePickerField.js +6 -2
  17. package/build/browser/action-form/fields/FilePickerField.js.map +1 -1
  18. package/build/browser/action-form/fields/FilePickerField.module.css +4 -0
  19. package/build/browser/action-form/fields/FormFieldRenderer.js +24 -12
  20. package/build/browser/action-form/fields/FormFieldRenderer.js.map +1 -1
  21. package/build/browser/action-form/fields/NumberInputField.js +12 -8
  22. package/build/browser/action-form/fields/NumberInputField.js.map +1 -1
  23. package/build/browser/action-form/fields/NumberInputField.module.css +4 -0
  24. package/build/browser/action-form/fields/TextAreaField.js +3 -1
  25. package/build/browser/action-form/fields/TextAreaField.js.map +1 -1
  26. package/build/browser/action-form/fields/TextInputField.js +3 -1
  27. package/build/browser/action-form/fields/TextInputField.js.map +1 -1
  28. package/build/browser/action-form/utils/extractValidationRules.js +186 -0
  29. package/build/browser/action-form/utils/extractValidationRules.js.map +1 -0
  30. package/build/browser/public/experimental.js.map +1 -1
  31. package/build/browser/shared/hooks/useAsyncAction.js +56 -0
  32. package/build/browser/shared/hooks/useAsyncAction.js.map +1 -0
  33. package/build/browser/shared/hooks/useIsMounted.js +32 -0
  34. package/build/browser/shared/hooks/useIsMounted.js.map +1 -0
  35. package/build/browser/styles.css +37 -1
  36. package/build/cjs/public/experimental.cjs +361 -37
  37. package/build/cjs/public/experimental.cjs.map +1 -1
  38. package/build/cjs/public/experimental.css +30 -1
  39. package/build/cjs/public/experimental.css.map +1 -1
  40. package/build/cjs/public/experimental.d.cts +40 -13
  41. package/build/esm/action-form/ActionFormApi.js.map +1 -1
  42. package/build/esm/action-form/BaseForm.js +100 -12
  43. package/build/esm/action-form/BaseForm.js.map +1 -1
  44. package/build/esm/action-form/BaseForm.module.css +21 -1
  45. package/build/esm/action-form/FormField.js +3 -1
  46. package/build/esm/action-form/FormField.js.map +1 -1
  47. package/build/esm/action-form/FormFieldApi.js.map +1 -1
  48. package/build/esm/action-form/fields/BaseInput.module.css +4 -0
  49. package/build/esm/action-form/fields/DatetimePickerField.js +3 -1
  50. package/build/esm/action-form/fields/DatetimePickerField.js.map +1 -1
  51. package/build/esm/action-form/fields/DatetimePickerField.module.css +4 -0
  52. package/build/esm/action-form/fields/FieldBridge.js +31 -4
  53. package/build/esm/action-form/fields/FieldBridge.js.map +1 -1
  54. package/build/esm/action-form/fields/FilePickerField.js +6 -2
  55. package/build/esm/action-form/fields/FilePickerField.js.map +1 -1
  56. package/build/esm/action-form/fields/FilePickerField.module.css +4 -0
  57. package/build/esm/action-form/fields/FormFieldRenderer.js +24 -12
  58. package/build/esm/action-form/fields/FormFieldRenderer.js.map +1 -1
  59. package/build/esm/action-form/fields/NumberInputField.js +12 -8
  60. package/build/esm/action-form/fields/NumberInputField.js.map +1 -1
  61. package/build/esm/action-form/fields/NumberInputField.module.css +4 -0
  62. package/build/esm/action-form/fields/TextAreaField.js +3 -1
  63. package/build/esm/action-form/fields/TextAreaField.js.map +1 -1
  64. package/build/esm/action-form/fields/TextInputField.js +3 -1
  65. package/build/esm/action-form/fields/TextInputField.js.map +1 -1
  66. package/build/esm/action-form/utils/extractValidationRules.js +186 -0
  67. package/build/esm/action-form/utils/extractValidationRules.js.map +1 -0
  68. package/build/esm/public/experimental.js.map +1 -1
  69. package/build/esm/shared/hooks/useAsyncAction.js +56 -0
  70. package/build/esm/shared/hooks/useAsyncAction.js.map +1 -0
  71. package/build/esm/shared/hooks/useIsMounted.js +32 -0
  72. package/build/esm/shared/hooks/useIsMounted.js.map +1 -0
  73. package/build/types/action-form/ActionFormApi.d.ts +1 -1
  74. package/build/types/action-form/ActionFormApi.d.ts.map +1 -1
  75. package/build/types/action-form/BaseForm.d.ts.map +1 -1
  76. package/build/types/action-form/FormField.d.ts +1 -0
  77. package/build/types/action-form/FormField.d.ts.map +1 -1
  78. package/build/types/action-form/FormFieldApi.d.ts +39 -12
  79. package/build/types/action-form/FormFieldApi.d.ts.map +1 -1
  80. package/build/types/action-form/fields/DatetimePickerField.d.ts +1 -1
  81. package/build/types/action-form/fields/DatetimePickerField.d.ts.map +1 -1
  82. package/build/types/action-form/fields/FieldBridge.d.ts.map +1 -1
  83. package/build/types/action-form/fields/FormFieldRenderer.d.ts +2 -0
  84. package/build/types/action-form/fields/FormFieldRenderer.d.ts.map +1 -1
  85. package/build/types/action-form/fields/NumberInputField.d.ts +1 -1
  86. package/build/types/action-form/fields/NumberInputField.d.ts.map +1 -1
  87. package/build/types/action-form/fields/TextAreaField.d.ts +1 -1
  88. package/build/types/action-form/fields/TextAreaField.d.ts.map +1 -1
  89. package/build/types/action-form/fields/TextInputField.d.ts +1 -1
  90. package/build/types/action-form/fields/TextInputField.d.ts.map +1 -1
  91. package/build/types/action-form/utils/extractValidationRules.d.ts +13 -0
  92. package/build/types/action-form/utils/extractValidationRules.d.ts.map +1 -0
  93. package/build/types/public/experimental.d.ts +1 -1
  94. package/build/types/public/experimental.d.ts.map +1 -1
  95. package/build/types/shared/hooks/useAsyncAction.d.ts +16 -0
  96. package/build/types/shared/hooks/useAsyncAction.d.ts.map +1 -0
  97. package/build/types/shared/hooks/useIsMounted.d.ts +5 -0
  98. package/build/types/shared/hooks/useIsMounted.d.ts.map +1 -0
  99. package/package.json +8 -8
@@ -8801,10 +8801,206 @@ function PdfViewer({
8801
8801
  }, pdfViewerProps));
8802
8802
  }
8803
8803
  var typedReactMemo = React75__default.default.memo;
8804
+ function useIsMounted() {
8805
+ const isMountedRef = React75.useRef(true);
8806
+ React75.useEffect(function trackMountedState() {
8807
+ return () => {
8808
+ isMountedRef.current = false;
8809
+ };
8810
+ }, []);
8811
+ return isMountedRef;
8812
+ }
8813
+
8814
+ // src/shared/hooks/useAsyncAction.ts
8815
+ function useAsyncAction(action) {
8816
+ const [isPending, setIsPending] = React75.useState(false);
8817
+ const [error, setError] = React75.useState(void 0);
8818
+ const isMountedRef = useIsMounted();
8819
+ const execute = React75.useCallback(async (...args) => {
8820
+ setError(void 0);
8821
+ setIsPending(true);
8822
+ try {
8823
+ await action(...args);
8824
+ } catch (err) {
8825
+ if (isMountedRef.current) {
8826
+ setError(err);
8827
+ }
8828
+ } finally {
8829
+ if (isMountedRef.current) {
8830
+ setIsPending(false);
8831
+ }
8832
+ }
8833
+ }, [action, isMountedRef]);
8834
+ const clearError = React75.useCallback(() => {
8835
+ setError(void 0);
8836
+ }, []);
8837
+ return {
8838
+ isPending,
8839
+ error,
8840
+ execute,
8841
+ clearError
8842
+ };
8843
+ }
8804
8844
 
8805
8845
  // src/action-form/BaseForm.module.css
8806
8846
  var BaseForm_default = {};
8807
8847
 
8848
+ // src/action-form/utils/extractValidationRules.ts
8849
+ function extractValidationRules(fieldDef) {
8850
+ const rules = {};
8851
+ if (fieldDef.isRequired) {
8852
+ rules.required = getMessage(fieldDef, {
8853
+ type: "required"
8854
+ });
8855
+ }
8856
+ const validateFns = {};
8857
+ switch (fieldDef.fieldComponent) {
8858
+ case "NUMBER_INPUT": {
8859
+ const {
8860
+ min,
8861
+ max
8862
+ } = fieldDef.fieldComponentProps;
8863
+ if (min != null) {
8864
+ const msg = getMessage(fieldDef, {
8865
+ type: "min",
8866
+ min
8867
+ });
8868
+ validateFns.min = (value) => typeof value === "number" && value < min ? msg : true;
8869
+ }
8870
+ if (max != null) {
8871
+ const msg = getMessage(fieldDef, {
8872
+ type: "max",
8873
+ max
8874
+ });
8875
+ validateFns.max = (value) => typeof value === "number" && value > max ? msg : true;
8876
+ }
8877
+ break;
8878
+ }
8879
+ case "TEXT_INPUT":
8880
+ case "TEXT_AREA": {
8881
+ const {
8882
+ minLength,
8883
+ maxLength
8884
+ } = fieldDef.fieldComponentProps;
8885
+ if (minLength != null) {
8886
+ rules.minLength = {
8887
+ value: minLength,
8888
+ message: getMessage(fieldDef, {
8889
+ type: "minLength",
8890
+ minLength
8891
+ })
8892
+ };
8893
+ }
8894
+ if (maxLength != null) {
8895
+ rules.maxLength = {
8896
+ value: maxLength,
8897
+ message: getMessage(fieldDef, {
8898
+ type: "maxLength",
8899
+ maxLength
8900
+ })
8901
+ };
8902
+ }
8903
+ break;
8904
+ }
8905
+ case "DATETIME_PICKER": {
8906
+ const {
8907
+ min,
8908
+ max
8909
+ } = fieldDef.fieldComponentProps;
8910
+ if (min != null) {
8911
+ const msg = getMessage(fieldDef, {
8912
+ type: "min",
8913
+ min
8914
+ });
8915
+ validateFns.min = (value) => value instanceof Date && value.getTime() < min.getTime() ? msg : true;
8916
+ }
8917
+ if (max != null) {
8918
+ const msg = getMessage(fieldDef, {
8919
+ type: "max",
8920
+ max
8921
+ });
8922
+ validateFns.max = (value) => value instanceof Date && value.getTime() > max.getTime() ? msg : true;
8923
+ }
8924
+ break;
8925
+ }
8926
+ case "FILE_PICKER": {
8927
+ const {
8928
+ maxSize
8929
+ } = fieldDef.fieldComponentProps;
8930
+ if (maxSize != null) {
8931
+ const msg = getMessage(fieldDef, {
8932
+ type: "maxSize",
8933
+ maxSize
8934
+ });
8935
+ validateFns.maxSize = (value) => {
8936
+ if (value instanceof File) {
8937
+ return value.size > maxSize ? msg : true;
8938
+ }
8939
+ if (Array.isArray(value)) {
8940
+ const oversized = value.some((f) => f instanceof File && f.size > maxSize);
8941
+ return oversized ? msg : true;
8942
+ }
8943
+ return true;
8944
+ };
8945
+ }
8946
+ break;
8947
+ }
8948
+ }
8949
+ if (fieldDef.validate != null) {
8950
+ const userValidate = fieldDef.validate;
8951
+ validateFns.custom = async (value) => {
8952
+ const result = await userValidate(value);
8953
+ if (result == null) {
8954
+ return true;
8955
+ }
8956
+ return getMessage(fieldDef, {
8957
+ type: "validate",
8958
+ message: result
8959
+ });
8960
+ };
8961
+ }
8962
+ if (Object.keys(validateFns).length > 0) {
8963
+ rules.validate = validateFns;
8964
+ }
8965
+ return rules;
8966
+ }
8967
+ function getMessage(fieldDef, error) {
8968
+ return fieldDef.onValidationError?.(error) ?? getDefaultMessage(error);
8969
+ }
8970
+ function getDefaultMessage(error) {
8971
+ switch (error.type) {
8972
+ case "required":
8973
+ return "This field is required";
8974
+ case "min":
8975
+ return `Must be at least ${formatConstraint(error.min)}`;
8976
+ case "max":
8977
+ return `Must be at most ${formatConstraint(error.max)}`;
8978
+ case "minLength":
8979
+ return `Must be at least ${error.minLength} characters`;
8980
+ case "maxLength":
8981
+ return `Must be at most ${error.maxLength} characters`;
8982
+ case "maxSize":
8983
+ return `File must be smaller than ${formatBytes(error.maxSize)}`;
8984
+ case "validate":
8985
+ return error.message;
8986
+ }
8987
+ }
8988
+ function formatConstraint(value) {
8989
+ if (value instanceof Date) {
8990
+ return value.toLocaleDateString();
8991
+ }
8992
+ return String(value);
8993
+ }
8994
+ function formatBytes(bytes) {
8995
+ if (bytes < 1024) {
8996
+ return `${bytes} B`;
8997
+ }
8998
+ if (bytes < 1024 * 1024) {
8999
+ return `${(bytes / 1024).toFixed(1)} KB`;
9000
+ }
9001
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
9002
+ }
9003
+
8808
9004
  // src/action-form/FormField.module.css
8809
9005
  var FormField_default = {};
8810
9006
 
@@ -8815,10 +9011,12 @@ var FormField = /* @__PURE__ */ React75.memo(function FormFieldFn({
8815
9011
  isRequired,
8816
9012
  helperText,
8817
9013
  error,
9014
+ onBlur,
8818
9015
  children
8819
9016
  }) {
8820
9017
  return /* @__PURE__ */ React75__default.default.createElement("div", {
8821
- className: FormField_default.osdkFormField
9018
+ className: FormField_default.osdkFormField,
9019
+ onBlur
8822
9020
  }, label != null && /* @__PURE__ */ React75__default.default.createElement("label", {
8823
9021
  className: FormField_default.osdkFormFieldLabel,
8824
9022
  htmlFor: fieldKey
@@ -8859,6 +9057,7 @@ function DatetimePickerField({
8859
9057
  id,
8860
9058
  value,
8861
9059
  onChange,
9060
+ error,
8862
9061
  min,
8863
9062
  max,
8864
9063
  placeholder,
@@ -8910,7 +9109,8 @@ function DatetimePickerField({
8910
9109
  }, /* @__PURE__ */ React75__default.default.createElement(popover.Popover.Trigger, {
8911
9110
  id,
8912
9111
  className: classnames13__default.default(DatetimePickerField_default.triggerButton, !hasValue && DatetimePickerField_default.triggerButtonPlaceholder),
8913
- "aria-label": hasValue ? void 0 : "Select date"
9112
+ "aria-label": hasValue ? void 0 : "Select date",
9113
+ "aria-invalid": error != null || void 0
8914
9114
  }, displayText), /* @__PURE__ */ React75__default.default.createElement(popover.Popover.Portal, null, /* @__PURE__ */ React75__default.default.createElement(popover.Popover.Positioner, {
8915
9115
  sideOffset: 4
8916
9116
  }, /* @__PURE__ */ React75__default.default.createElement(popover.Popover.Popup, {
@@ -9099,9 +9299,12 @@ var FilePickerField = /* @__PURE__ */ React75.memo(function FilePickerFieldFn({
9099
9299
  id,
9100
9300
  value,
9101
9301
  onChange,
9302
+ error,
9102
9303
  isMulti,
9103
9304
  accept,
9104
- // TODO: implement maxSize validation in a follow-up
9305
+ // maxSize is enforced by form-level validation (extractValidationRules),
9306
+ // not here. Silently dropping oversized files would leave the user with
9307
+ // no indication of why their selection disappeared.
9105
9308
  maxSize: _maxSize,
9106
9309
  text = "No file chosen",
9107
9310
  buttonText = "Browse"
@@ -9152,7 +9355,8 @@ var FilePickerField = /* @__PURE__ */ React75.memo(function FilePickerFieldFn({
9152
9355
  role: "button",
9153
9356
  "aria-label": "Choose file",
9154
9357
  onClick: openFileDialog,
9155
- onKeyDown: handleKeyDown
9358
+ onKeyDown: handleKeyDown,
9359
+ "aria-invalid": error != null || void 0
9156
9360
  }, /* @__PURE__ */ React75__default.default.createElement("input", {
9157
9361
  ref: inputRef,
9158
9362
  type: "file",
@@ -9202,9 +9406,10 @@ function NumberInputField({
9202
9406
  id,
9203
9407
  value,
9204
9408
  onChange,
9409
+ error,
9205
9410
  placeholder,
9206
- min: _min,
9207
- max: _max,
9411
+ min,
9412
+ max,
9208
9413
  step
9209
9414
  }) {
9210
9415
  const [displayValue, setDisplayValue] = React75.useState(() => formatNumberForDisplay(value));
@@ -9226,11 +9431,11 @@ function NumberInputField({
9226
9431
  const applyStep = React75.useCallback((direction) => {
9227
9432
  const current = parseNumericValue(displayValue) ?? 0;
9228
9433
  const delta = direction * (step ?? DEFAULT_STEP);
9229
- const next = current + delta;
9434
+ const next = clamp(current + delta, min, max);
9230
9435
  const formatted = formatNumberForDisplay(next);
9231
9436
  setDisplayValue(formatted);
9232
9437
  onChange?.(next);
9233
- }, [displayValue, onChange, step]);
9438
+ }, [displayValue, onChange, step, min, max]);
9234
9439
  const handleKeyDown = React75.useCallback((e) => {
9235
9440
  if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
9236
9441
  return;
@@ -9245,7 +9450,8 @@ function NumberInputField({
9245
9450
  applyStep(-1);
9246
9451
  }, [applyStep]);
9247
9452
  return /* @__PURE__ */ React75__default.default.createElement("div", {
9248
- className: NumberInputField_default.osdkNumberInputWrapper
9453
+ className: NumberInputField_default.osdkNumberInputWrapper,
9454
+ "aria-invalid": error != null || void 0
9249
9455
  }, /* @__PURE__ */ React75__default.default.createElement(input.Input, {
9250
9456
  id,
9251
9457
  className: NumberInputField_default.osdkNumberInputField,
@@ -9286,6 +9492,11 @@ function parseNumericValue(text) {
9286
9492
  function formatNumberForDisplay(value) {
9287
9493
  return value != null ? String(value) : "";
9288
9494
  }
9495
+ function clamp(value, min, max) {
9496
+ if (min != null && value < min) return min;
9497
+ if (max != null && value > max) return max;
9498
+ return value;
9499
+ }
9289
9500
  var BlueprintIcon = /* @__PURE__ */ React75__default.default.memo(function BlueprintIconFn({
9290
9501
  icon,
9291
9502
  size
@@ -9472,6 +9683,7 @@ function TextAreaField({
9472
9683
  id,
9473
9684
  value,
9474
9685
  onChange,
9686
+ error,
9475
9687
  placeholder,
9476
9688
  rows,
9477
9689
  wrap,
@@ -9491,13 +9703,15 @@ function TextAreaField({
9491
9703
  placeholder,
9492
9704
  minLength,
9493
9705
  maxLength,
9494
- render: renderTextarea
9706
+ render: renderTextarea,
9707
+ "aria-invalid": error != null || void 0
9495
9708
  });
9496
9709
  }
9497
9710
  function TextInputField({
9498
9711
  id,
9499
9712
  value,
9500
9713
  onChange,
9714
+ error,
9501
9715
  placeholder,
9502
9716
  minLength,
9503
9717
  maxLength
@@ -9510,7 +9724,8 @@ function TextInputField({
9510
9724
  onValueChange: onChange,
9511
9725
  placeholder,
9512
9726
  minLength,
9513
- maxLength
9727
+ maxLength,
9728
+ "aria-invalid": error != null || void 0
9514
9729
  });
9515
9730
  }
9516
9731
 
@@ -9527,7 +9742,9 @@ function _extends13() {
9527
9742
  var FormFieldRenderer = /* @__PURE__ */ React75.memo(function FormFieldRendererFn({
9528
9743
  fieldDefinition,
9529
9744
  value,
9530
- onFieldValueChange
9745
+ onFieldValueChange,
9746
+ onBlur,
9747
+ error
9531
9748
  }) {
9532
9749
  const {
9533
9750
  label,
@@ -9539,30 +9756,35 @@ var FormFieldRenderer = /* @__PURE__ */ React75.memo(function FormFieldRendererF
9539
9756
  label,
9540
9757
  isRequired,
9541
9758
  fieldKey: fieldDefinition.fieldKey,
9542
- helperText: helperTextPlacement !== "tooltip" ? helperText : void 0
9543
- }, renderFieldComponent(fieldDefinition, value, onFieldValueChange));
9759
+ helperText: helperTextPlacement !== "tooltip" ? helperText : void 0,
9760
+ error,
9761
+ onBlur
9762
+ }, renderFieldComponent(fieldDefinition, value, onFieldValueChange, error));
9544
9763
  });
9545
- function renderFieldComponent(fieldDefinition, value, onChange) {
9764
+ function renderFieldComponent(fieldDefinition, value, onChange, error) {
9546
9765
  switch (fieldDefinition.fieldComponent) {
9547
9766
  case "TEXT_INPUT":
9548
9767
  return /* @__PURE__ */ React75__default.default.createElement(TextInputField, _extends13({
9549
9768
  id: fieldDefinition.fieldKey,
9550
9769
  value: value != null ? String(value) : "",
9551
9770
  onChange,
9552
- placeholder: fieldDefinition.placeholder
9771
+ placeholder: fieldDefinition.placeholder,
9772
+ error
9553
9773
  }, fieldDefinition.fieldComponentProps));
9554
9774
  case "TEXT_AREA":
9555
9775
  return /* @__PURE__ */ React75__default.default.createElement(TextAreaField, _extends13({
9556
9776
  id: fieldDefinition.fieldKey,
9557
9777
  value: value != null ? String(value) : "",
9558
9778
  onChange,
9559
- placeholder: fieldDefinition.placeholder
9779
+ placeholder: fieldDefinition.placeholder,
9780
+ error
9560
9781
  }, fieldDefinition.fieldComponentProps));
9561
9782
  case "DROPDOWN": {
9562
9783
  return /* @__PURE__ */ React75__default.default.createElement(DropdownField, _extends13({
9563
9784
  value,
9564
9785
  onChange,
9565
- placeholder: fieldDefinition.placeholder
9786
+ placeholder: fieldDefinition.placeholder,
9787
+ error
9566
9788
  }, fieldDefinition.fieldComponentProps));
9567
9789
  }
9568
9790
  case "DATETIME_PICKER":
@@ -9570,32 +9792,37 @@ function renderFieldComponent(fieldDefinition, value, onChange) {
9570
9792
  id: fieldDefinition.fieldKey,
9571
9793
  placeholder: fieldDefinition.placeholder,
9572
9794
  value: value instanceof Date ? value : null,
9573
- onChange
9795
+ onChange,
9796
+ error
9574
9797
  }, fieldDefinition.fieldComponentProps));
9575
9798
  case "RADIO_BUTTONS":
9576
9799
  return /* @__PURE__ */ React75__default.default.createElement(RadioButtonsField, _extends13({
9577
9800
  id: fieldDefinition.fieldKey,
9578
9801
  value,
9579
- onChange
9802
+ onChange,
9803
+ error
9580
9804
  }, fieldDefinition.fieldComponentProps));
9581
9805
  case "CUSTOM":
9582
9806
  return /* @__PURE__ */ React75__default.default.createElement(CustomField, _extends13({
9583
9807
  id: fieldDefinition.fieldKey,
9584
9808
  value,
9585
- onChange
9809
+ onChange,
9810
+ error
9586
9811
  }, fieldDefinition.fieldComponentProps));
9587
9812
  case "NUMBER_INPUT":
9588
9813
  return /* @__PURE__ */ React75__default.default.createElement(NumberInputField, _extends13({
9589
9814
  id: fieldDefinition.fieldKey,
9590
9815
  value: typeof value === "number" ? value : null,
9591
9816
  onChange,
9592
- placeholder: fieldDefinition.placeholder
9817
+ placeholder: fieldDefinition.placeholder,
9818
+ error
9593
9819
  }, fieldDefinition.fieldComponentProps));
9594
9820
  case "FILE_PICKER":
9595
9821
  return /* @__PURE__ */ React75__default.default.createElement(FilePickerField, _extends13({
9596
9822
  id: fieldDefinition.fieldKey,
9597
9823
  value: coerceToFileValue(value),
9598
- onChange
9824
+ onChange,
9825
+ error
9599
9826
  }, fieldDefinition.fieldComponentProps));
9600
9827
  case "OBJECT_SET":
9601
9828
  return /* @__PURE__ */ React75__default.default.createElement(ObjectSetField, _extends13({
@@ -9622,28 +9849,47 @@ function assertUnreachableFieldComponent(value) {
9622
9849
  }
9623
9850
 
9624
9851
  // src/action-form/fields/FieldBridge.tsx
9852
+ var SELECT_LIKE_FIELDS = /* @__PURE__ */ new Set(["RADIO_BUTTONS", "DROPDOWN"]);
9625
9853
  var FieldBridge = /* @__PURE__ */ React75.memo(function FieldBridgeFn({
9626
9854
  fieldDef,
9627
9855
  control,
9628
9856
  onExternalChange
9629
9857
  }) {
9858
+ const rules = React75.useMemo(() => extractValidationRules(fieldDef), [fieldDef]);
9630
9859
  const {
9631
9860
  field: {
9632
9861
  onChange,
9862
+ onBlur,
9633
9863
  value
9864
+ },
9865
+ fieldState: {
9866
+ error: fieldError
9634
9867
  }
9635
9868
  } = reactHookForm.useController({
9636
9869
  name: fieldDef.fieldKey,
9637
- control
9870
+ control,
9871
+ rules
9638
9872
  });
9873
+ const isSelectLike = SELECT_LIKE_FIELDS.has(fieldDef.fieldComponent);
9639
9874
  const handleChange = React75.useCallback((newValue) => {
9640
9875
  onChange(newValue);
9641
9876
  onExternalChange?.(fieldDef.fieldKey, newValue);
9642
- }, [onChange, onExternalChange, fieldDef.fieldKey]);
9877
+ if (isSelectLike) {
9878
+ onBlur();
9879
+ }
9880
+ }, [onChange, onBlur, onExternalChange, fieldDef.fieldKey, isSelectLike]);
9881
+ const handleBlur = React75.useCallback((e) => {
9882
+ if (e.currentTarget.contains(e.relatedTarget)) {
9883
+ return;
9884
+ }
9885
+ onBlur();
9886
+ }, [onBlur]);
9643
9887
  return /* @__PURE__ */ React75__default.default.createElement(FormFieldRenderer, {
9644
9888
  value,
9645
9889
  fieldDefinition: fieldDef,
9646
- onFieldValueChange: handleChange
9890
+ onFieldValueChange: handleChange,
9891
+ onBlur: handleBlur,
9892
+ error: fieldError?.message
9647
9893
  });
9648
9894
  });
9649
9895
 
@@ -9660,6 +9906,7 @@ var FormHeader = /* @__PURE__ */ React75.memo(function FormHeaderFn({
9660
9906
  });
9661
9907
 
9662
9908
  // src/action-form/BaseForm.tsx
9909
+ var TOOLTIP_TRIGGER_DELAY_MS = 200;
9663
9910
  var BaseForm = /* @__PURE__ */ React75.memo(function BaseFormFn({
9664
9911
  formTitle,
9665
9912
  fieldDefinitions,
@@ -9675,20 +9922,54 @@ var BaseForm = /* @__PURE__ */ React75.memo(function BaseFormFn({
9675
9922
  const defaultValues = React75.useMemo(() => buildDefaultValues(fieldDefinitions), [fieldDefinitions]);
9676
9923
  const {
9677
9924
  control,
9678
- handleSubmit: rhfHandleSubmit
9925
+ trigger,
9926
+ getValues,
9927
+ formState: {
9928
+ errors
9929
+ }
9679
9930
  } = reactHookForm.useForm({
9931
+ // Validate on blur first, then revalidate on change after the first
9932
+ // error. This gives the user a chance to finish typing before seeing
9933
+ // errors, while staying responsive once an error is surfaced.
9934
+ mode: "onTouched",
9680
9935
  ...isControlled ? {
9681
9936
  values: controlledFormState
9682
9937
  } : {
9683
9938
  defaultValues
9684
9939
  }
9685
9940
  });
9686
- const onFormSubmit = React75.useCallback((rhfValues) => {
9687
- onSubmit(controlledFormState ?? rhfValues);
9688
- }, [onSubmit, controlledFormState]);
9941
+ const [hasAttemptedSubmit, setHasAttemptedSubmit] = React75.useState(false);
9942
+ const {
9943
+ isPending: isSubmitting,
9944
+ error: submissionError,
9945
+ execute: executeSubmit,
9946
+ clearError
9947
+ } = useAsyncAction(onSubmit);
9948
+ const submissionErrorMessage = submissionError != null ? submissionError instanceof Error ? submissionError.message : "Submission failed" : void 0;
9949
+ const handleFormSubmit = React75.useCallback(async (e) => {
9950
+ e.preventDefault();
9951
+ setHasAttemptedSubmit(true);
9952
+ const isValid = await trigger();
9953
+ if (!isValid) {
9954
+ return;
9955
+ }
9956
+ await executeSubmit(controlledFormState ?? getValues());
9957
+ }, [trigger, executeSubmit, controlledFormState, getValues]);
9958
+ const handleFieldChange = React75.useCallback((fieldKey, value) => {
9959
+ clearError();
9960
+ onFieldValueChange?.(fieldKey, value);
9961
+ }, [clearError, onFieldValueChange]);
9962
+ const isFormPending = isPending || isSubmitting;
9963
+ const labelByFieldKey = React75.useMemo(() => new Map(fieldDefinitions.map((d) => [d.fieldKey, d.label])), [fieldDefinitions]);
9964
+ const errorEntries = Object.entries(errors).map(([key, entry]) => ({
9965
+ label: labelByFieldKey.get(key) ?? key,
9966
+ message: entry?.message ?? "Invalid"
9967
+ }));
9968
+ const areErrorsPresent = errorEntries.length > 0;
9969
+ const buttonErrorMessage = areErrorsPresent ? "Some fields are invalid" : submissionErrorMessage;
9689
9970
  return /* @__PURE__ */ React75__default.default.createElement("form", {
9690
9971
  className: classnames13__default.default(BaseForm_default.osdkForm, className),
9691
- onSubmit: rhfHandleSubmit(onFormSubmit)
9972
+ onSubmit: handleFormSubmit
9692
9973
  }, formTitle != null && /* @__PURE__ */ React75__default.default.createElement(FormHeader, {
9693
9974
  title: formTitle
9694
9975
  }), isLoading && fieldDefinitions.length === 0 && /* @__PURE__ */ React75__default.default.createElement("div", {
@@ -9699,14 +9980,18 @@ var BaseForm = /* @__PURE__ */ React75.memo(function BaseFormFn({
9699
9980
  key: fieldDef.fieldKey,
9700
9981
  fieldDef,
9701
9982
  control,
9702
- onExternalChange: onFieldValueChange
9983
+ onExternalChange: handleFieldChange
9703
9984
  }))), /* @__PURE__ */ React75__default.default.createElement("div", {
9704
9985
  className: BaseForm_default.osdkFormFooter
9705
- }, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.ActionButton, {
9706
- type: "submit",
9707
- variant: "primary",
9708
- disabled: isSubmitDisabled || isPending
9709
- }, isPending ? "Submitting\u2026" : "Submit")));
9986
+ }, /* @__PURE__ */ React75__default.default.createElement(ErrorIndicator, {
9987
+ errorEntries
9988
+ }), /* @__PURE__ */ React75__default.default.createElement("div", {
9989
+ className: BaseForm_default.osdkFormSubmitButton
9990
+ }, /* @__PURE__ */ React75__default.default.createElement(SubmitButton, {
9991
+ isPending: isFormPending,
9992
+ isSubmitDisabled: isSubmitDisabled || hasAttemptedSubmit && areErrorsPresent,
9993
+ errorMessage: buttonErrorMessage
9994
+ }))));
9710
9995
  });
9711
9996
  function buildDefaultValues(fieldDefinitions) {
9712
9997
  const values = {};
@@ -9718,6 +10003,45 @@ function buildDefaultValues(fieldDefinitions) {
9718
10003
  }
9719
10004
  return values;
9720
10005
  }
10006
+ var SubmitButton = /* @__PURE__ */ React75.memo(function SubmitButtonFn({
10007
+ isPending,
10008
+ isSubmitDisabled,
10009
+ errorMessage
10010
+ }) {
10011
+ const buttonLabel = isPending ? "Submitting\u2026" : "Submit";
10012
+ const button = /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.ActionButton, {
10013
+ type: "submit",
10014
+ variant: "primary",
10015
+ disabled: isSubmitDisabled || isPending
10016
+ }, buttonLabel);
10017
+ if (errorMessage == null) {
10018
+ return button;
10019
+ }
10020
+ return /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Root, {
10021
+ defaultOpen: true
10022
+ }, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Trigger, {
10023
+ delay: TOOLTIP_TRIGGER_DELAY_MS
10024
+ }, button), /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Portal, null, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Positioner, null, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Popup, null, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Arrow, null), errorMessage))));
10025
+ });
10026
+ function ErrorIndicator({
10027
+ errorEntries
10028
+ }) {
10029
+ if (errorEntries.length === 0) {
10030
+ return null;
10031
+ }
10032
+ const count = errorEntries.length;
10033
+ return /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Root, null, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Trigger, {
10034
+ delay: TOOLTIP_TRIGGER_DELAY_MS
10035
+ }, /* @__PURE__ */ React75__default.default.createElement("span", {
10036
+ className: BaseForm_default.osdkFormErrorIndicator
10037
+ }, /* @__PURE__ */ React75__default.default.createElement(icons.Error, {
10038
+ size: 14
10039
+ }), count === 1 ? "1 issue" : `${count} issues`)), /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Portal, null, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Positioner, null, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Popup, null, /* @__PURE__ */ React75__default.default.createElement(chunkNEWX2ZXY_cjs.Tooltip.Arrow, null), /* @__PURE__ */ React75__default.default.createElement("ul", {
10040
+ className: BaseForm_default.osdkFormErrorList
10041
+ }, errorEntries.map((entry) => /* @__PURE__ */ React75__default.default.createElement("li", {
10042
+ key: entry.label
10043
+ }, /* @__PURE__ */ React75__default.default.createElement("strong", null, entry.label, ":"), " ", entry.message)))))));
10044
+ }
9721
10045
 
9722
10046
  // src/action-form/utils/coerceFieldValue.ts
9723
10047
  function coerceFieldValue(parameterType, rawValue) {