@object-ui/fields 16.0.0 → 17.1.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 (71) hide show
  1. package/CHANGELOG.md +630 -0
  2. package/README.md +45 -7
  3. package/dist/FieldEditWidget.d.ts +7 -0
  4. package/dist/field-type-alias.d.ts +16 -0
  5. package/dist/index.cjs +5 -5
  6. package/dist/index.d.ts +35 -11
  7. package/dist/index.js +2241 -1853
  8. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  9. package/dist/widgets/CheckboxesField.d.ts +8 -1
  10. package/dist/widgets/FileField.d.ts +1 -1
  11. package/dist/widgets/FilterConditionField.d.ts +32 -0
  12. package/dist/widgets/ImageField.d.ts +1 -1
  13. package/dist/widgets/ImageLightbox.d.ts +30 -0
  14. package/dist/widgets/ImageLightbox.test.d.ts +1 -0
  15. package/dist/widgets/LookupField.hydration.test.d.ts +1 -0
  16. package/dist/widgets/LookupField.idField.test.d.ts +1 -0
  17. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  18. package/dist/widgets/MultiSelectField.d.ts +10 -2
  19. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  20. package/dist/widgets/RadioField.d.ts +8 -1
  21. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  22. package/dist/widgets/SelectField.d.ts +13 -9
  23. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +8 -0
  24. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +8 -0
  25. package/dist/widgets/deriveLookupColumns.d.ts +1 -0
  26. package/dist/widgets/file-value.d.ts +112 -0
  27. package/dist/widgets/file-value.test.d.ts +1 -0
  28. package/dist/widgets/types.d.ts +6 -0
  29. package/dist/widgets/useCascadingOptions.d.ts +18 -0
  30. package/dist/widgets/useFieldTranslation.d.ts +2 -0
  31. package/dist/widgets/useUploadingSignal.d.ts +9 -0
  32. package/dist/widgets/useUploadingSignal.test.d.ts +1 -0
  33. package/package.json +11 -10
  34. package/src/FieldEditWidget.test.ts +74 -2
  35. package/src/FieldEditWidget.tsx +44 -4
  36. package/src/field-type-alias.ts +100 -0
  37. package/src/field-type-coverage.test.ts +3 -1
  38. package/src/index.tsx +137 -135
  39. package/src/standard-widgets.test.tsx +44 -0
  40. package/src/widgets/CheckboxesField.cascade.test.tsx +143 -0
  41. package/src/widgets/CheckboxesField.tsx +77 -11
  42. package/src/widgets/FileField.tsx +91 -52
  43. package/src/widgets/FilterConditionField.tsx +109 -9
  44. package/src/widgets/ImageField.tsx +106 -65
  45. package/src/widgets/ImageLightbox.test.tsx +52 -0
  46. package/src/widgets/ImageLightbox.tsx +108 -0
  47. package/src/widgets/LookupField.hydration.test.tsx +136 -0
  48. package/src/widgets/LookupField.idField.test.tsx +61 -0
  49. package/src/widgets/LookupField.tsx +106 -9
  50. package/src/widgets/MultiSelectField.cascade.test.tsx +143 -0
  51. package/src/widgets/MultiSelectField.tsx +78 -11
  52. package/src/widgets/ObjectRefField.tsx +9 -4
  53. package/src/widgets/PeoplePicker.test.tsx +20 -0
  54. package/src/widgets/PeoplePicker.tsx +15 -3
  55. package/src/widgets/RadioField.cascade.test.tsx +141 -0
  56. package/src/widgets/RadioField.tsx +69 -9
  57. package/src/widgets/RecipientPickerField.test.tsx +157 -0
  58. package/src/widgets/RecipientPickerField.tsx +36 -16
  59. package/src/widgets/SelectField.tsx +33 -31
  60. package/src/widgets/TextAreaField.tsx +8 -3
  61. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +66 -0
  62. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +142 -0
  63. package/src/widgets/deriveLookupColumns.test.ts +13 -0
  64. package/src/widgets/deriveLookupColumns.ts +5 -15
  65. package/src/widgets/file-value.test.ts +230 -0
  66. package/src/widgets/file-value.ts +230 -0
  67. package/src/widgets/types.ts +6 -0
  68. package/src/widgets/useCascadingOptions.ts +46 -0
  69. package/src/widgets/useFieldTranslation.ts +30 -0
  70. package/src/widgets/useUploadingSignal.test.tsx +62 -0
  71. package/src/widgets/useUploadingSignal.ts +20 -0
@@ -0,0 +1,46 @@
1
+ import { useContext, useMemo } from 'react';
2
+ import {
3
+ resolveCascadingOptions,
4
+ type CascadingOptions,
5
+ type OptionLike,
6
+ type DependsOnInput,
7
+ } from '@object-ui/core';
8
+ import { SchemaRendererContext, usePredicateScope } from '@object-ui/react';
9
+
10
+ /**
11
+ * Shared per-option cascading / role-gating resolution for the option widgets
12
+ * (`SelectField` single, `MultiSelectField`, `RadioField`) — the client half of
13
+ * ADR-0058 (#2284). Each option may carry a `visibleWhen` CEL predicate; the
14
+ * offered set narrows against the live form record (`record.country == 'cn'`) +
15
+ * the global predicate scope (`'admin' in current_user.positions`). A field
16
+ * declares which sibling fields drive its list via `dependsOn`; while any is
17
+ * empty the list is *gated* — callers surface a "select the parent first" hint
18
+ * rather than an unfiltered set, mirroring the dependent-lookup UX.
19
+ *
20
+ * This is the React wrapper that sources `record` (the live form values) and the
21
+ * predicate scope from context; the actual resolution is the pure
22
+ * {@link resolveCascadingOptions} in `@object-ui/core`, shared with the form
23
+ * renderer so gating/filtering can never drift between them (#2715).
24
+ */
25
+ export type CascadingOptionsResult<T extends OptionLike> = CascadingOptions<T>;
26
+
27
+ export function useCascadingOptions<T extends OptionLike>(
28
+ rawOptions: readonly T[],
29
+ dependsOn: DependsOnInput,
30
+ dependentValues: Record<string, unknown> | undefined,
31
+ ): CascadingOptionsResult<T> {
32
+ // Live form values for cascading options — injected by the form renderer as
33
+ // `dependentValues` (same channel dependent lookups use), falling back to the
34
+ // record on SchemaRendererContext. `current_user` etc. come from the global
35
+ // predicate scope so role/context predicates resolve too.
36
+ const ctx = useContext(SchemaRendererContext) as any;
37
+ const record = useMemo<Record<string, unknown>>(() => {
38
+ return (dependentValues ?? ctx?.formValues ?? ctx?.data ?? {}) as Record<string, unknown>;
39
+ }, [dependentValues, ctx?.formValues, ctx?.data]);
40
+ const predicateScope = usePredicateScope();
41
+
42
+ return useMemo(
43
+ () => resolveCascadingOptions(rawOptions, record, dependsOn, predicateScope),
44
+ [rawOptions, record, dependsOn, predicateScope],
45
+ );
46
+ }
@@ -39,6 +39,36 @@ const FIELD_DEFAULTS: Record<string, string> = {
39
39
  'lookup.nextPage': 'Next page',
40
40
  'lookup.jumpToPage': 'Jump to page',
41
41
  'lookup.retry': 'Retry',
42
+ // objectstack#3821 — sharing-rule authoring widgets (object-ref /
43
+ // recipient-picker / filter-condition). The recipient placeholder is keyed
44
+ // PER TYPE rather than interpolating the enum value into an English
45
+ // sentence, which no locale could translate.
46
+ 'fields.objectRef.loading': 'Loading objects…',
47
+ 'fields.objectRef.placeholder': 'Select an object',
48
+ 'fields.objectRef.search': 'Search objects…',
49
+ 'fields.objectRef.empty': 'No objects found',
50
+ 'fields.recipient.selectTypeFirst': 'Select a recipient type first.',
51
+ 'fields.recipient.loading': 'Loading…',
52
+ 'fields.recipient.search': 'Search…',
53
+ 'fields.recipient.empty': 'No matches',
54
+ 'fields.recipient.select': 'Select a recipient',
55
+ 'fields.recipient.selectUser': 'Select a user',
56
+ 'fields.recipient.selectTeam': 'Select a team',
57
+ 'fields.recipient.selectBusinessUnit': 'Select a business unit',
58
+ 'fields.recipient.selectPosition': 'Select a position',
59
+ 'fields.recipient.selectUnitAndSubordinates': 'Select a business unit',
60
+ 'fields.filterCondition.selectObjectFirst': 'Select an object first.',
61
+ // objectstack#3896 — this used to be 'All records'. An empty criteria never
62
+ // meant "share everything"; it meant the predicate was missing, and the
63
+ // sharing evaluator failed open on it. Such a rule is now refused on save
64
+ // and shares nothing, so say that rather than advertise the old bug.
65
+ 'fields.filterCondition.noCriteria': 'No criteria — this rule shares nothing',
66
+ 'fields.filterCondition.criteriaRequired':
67
+ 'Add at least one condition. A rule with no criteria would share every record, so it cannot be saved.',
68
+ 'fields.filterCondition.invalidJson': 'Invalid JSON — the rule will match no records until fixed.',
69
+ 'fields.filterCondition.jsonOnly': 'This criteria can only be edited as JSON',
70
+ 'fields.filterCondition.editAsJson': 'Edit as JSON',
71
+ 'fields.filterCondition.useVisualBuilder': 'Use visual builder',
42
72
  // objectui#2600 B5 — capability picker scope group headers.
43
73
  'capability.group.platform': 'Platform',
44
74
  'capability.group.org': 'Organization',
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useUploadingSignal } from './useUploadingSignal';
4
+
5
+ /**
6
+ * useUploadingSignal — surfaces an upload widget's in-progress flips to a host
7
+ * (ActionParamDialog blocks Confirm while a file/image param uploads). It must
8
+ * fire only when `uploading` changes and always call the latest callback.
9
+ */
10
+ describe('useUploadingSignal', () => {
11
+ it('fires with the initial value on mount', () => {
12
+ const cb = vi.fn();
13
+ renderHook(() => useUploadingSignal(false, cb));
14
+ expect(cb).toHaveBeenCalledTimes(1);
15
+ expect(cb).toHaveBeenLastCalledWith(false);
16
+ });
17
+
18
+ it('fires again only when `uploading` changes', () => {
19
+ const cb = vi.fn();
20
+ const { rerender } = renderHook(({ u }) => useUploadingSignal(u, cb), {
21
+ initialProps: { u: false },
22
+ });
23
+ cb.mockClear();
24
+
25
+ rerender({ u: true });
26
+ expect(cb).toHaveBeenCalledTimes(1);
27
+ expect(cb).toHaveBeenLastCalledWith(true);
28
+
29
+ // Same value → no extra fire.
30
+ rerender({ u: true });
31
+ expect(cb).toHaveBeenCalledTimes(1);
32
+
33
+ rerender({ u: false });
34
+ expect(cb).toHaveBeenCalledTimes(2);
35
+ expect(cb).toHaveBeenLastCalledWith(false);
36
+ });
37
+
38
+ it('always invokes the latest callback (no stale closure)', () => {
39
+ const first = vi.fn();
40
+ const second = vi.fn();
41
+ const { rerender } = renderHook(
42
+ ({ cb, u }) => useUploadingSignal(u, cb),
43
+ { initialProps: { cb: first, u: false } },
44
+ );
45
+ // Swap the callback without changing `uploading` → no fire yet.
46
+ rerender({ cb: second, u: false });
47
+ expect(second).not.toHaveBeenCalled();
48
+ // Now flip `uploading` → the NEW callback fires, not the old one.
49
+ rerender({ cb: second, u: true });
50
+ expect(second).toHaveBeenCalledWith(true);
51
+ expect(first).not.toHaveBeenCalledWith(true);
52
+ });
53
+
54
+ it('is a no-op when no callback is provided', () => {
55
+ expect(() => {
56
+ const { rerender } = renderHook(({ u }) => useUploadingSignal(u), {
57
+ initialProps: { u: false },
58
+ });
59
+ rerender({ u: true });
60
+ }).not.toThrow();
61
+ });
62
+ });
@@ -0,0 +1,20 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ /**
4
+ * Notify a parent whenever an upload widget's in-progress state flips. Used by
5
+ * hosts that must block a submit until the presigned upload resolves — e.g.
6
+ * `ActionParamDialog` disables Confirm so a `file`/`image` param can't be
7
+ * submitted mid-upload (the value only becomes the fileId once the upload
8
+ * settles). Fires with the latest callback and only when `uploading` actually
9
+ * changes, so an inline arrow prop doesn't thrash the parent every render.
10
+ */
11
+ export function useUploadingSignal(
12
+ uploading: boolean,
13
+ onUploadingChange?: (uploading: boolean) => void,
14
+ ): void {
15
+ const ref = useRef(onUploadingChange);
16
+ ref.current = onUploadingChange;
17
+ useEffect(() => {
18
+ ref.current?.(uploading);
19
+ }, [uploading]);
20
+ }