@object-ui/fields 17.6.0 → 18.0.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 (64) hide show
  1. package/CHANGELOG.md +6228 -0
  2. package/README.md +251 -9
  3. package/dist/FieldEditWidget.d.ts +54 -6
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/fieldHelpText.d.ts +41 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +68 -5
  9. package/dist/index.d.ts +446 -69
  10. package/dist/index.js +5326 -3443
  11. package/dist/resolveActionParams.d.ts +259 -0
  12. package/dist/widgets/AvatarField.d.ts +19 -3
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/DateField.d.ts +1 -1
  15. package/dist/widgets/DateTimeField.d.ts +1 -1
  16. package/dist/widgets/FilterConditionField.d.ts +21 -7
  17. package/dist/widgets/FormulaField.d.ts +41 -0
  18. package/dist/widgets/GeolocationField.d.ts +9 -3
  19. package/dist/widgets/GridField.d.ts +73 -101
  20. package/dist/widgets/LocationField.d.ts +30 -3
  21. package/dist/widgets/LookupField.d.ts +8 -0
  22. package/dist/widgets/NumberField.d.ts +1 -1
  23. package/dist/widgets/ObjectField.d.ts +9 -2
  24. package/dist/widgets/OptionsEmptyState.d.ts +29 -5
  25. package/dist/widgets/PasswordField.d.ts +14 -0
  26. package/dist/widgets/PeoplePicker.d.ts +17 -2
  27. package/dist/widgets/PercentField.d.ts +2 -1
  28. package/dist/widgets/PersonRow.d.ts +13 -2
  29. package/dist/widgets/RecipientPickerField.d.ts +75 -0
  30. package/dist/widgets/RecordPickerDialog.d.ts +31 -12
  31. package/dist/widgets/RegistryRefField.d.ts +77 -0
  32. package/dist/widgets/RichTextField.d.ts +1 -1
  33. package/dist/widgets/SelectionTray.d.ts +13 -2
  34. package/dist/widgets/SummaryField.d.ts +30 -0
  35. package/dist/widgets/TextField.d.ts +1 -1
  36. package/dist/widgets/TimeField.d.ts +1 -1
  37. package/dist/widgets/booleanValueLabel.d.ts +18 -0
  38. package/dist/widgets/date-picker/DatePickerField.d.ts +40 -0
  39. package/dist/widgets/date-picker/localeDateText.d.ts +39 -0
  40. package/dist/widgets/deriveLookupColumns.d.ts +1 -1
  41. package/dist/widgets/file-affordance.d.ts +63 -0
  42. package/dist/widgets/file-refused.d.ts +17 -0
  43. package/dist/widgets/file-size-guard.d.ts +5 -23
  44. package/dist/widgets/file-value.d.ts +49 -9
  45. package/dist/widgets/longTextCarrierHandoff.d.ts +35 -0
  46. package/dist/widgets/lookupColumnDisplay.d.ts +26 -9
  47. package/dist/widgets/nativeDateValue.d.ts +9 -27
  48. package/dist/widgets/number-format.d.ts +137 -0
  49. package/dist/widgets/numberBadInput.d.ts +135 -0
  50. package/dist/widgets/objectRefLabel.d.ts +16 -0
  51. package/dist/widgets/personDisplay.d.ts +22 -4
  52. package/dist/widgets/recipientKinds.d.ts +85 -0
  53. package/dist/widgets/richTextDisplay.d.ts +32 -13
  54. package/dist/widgets/searchOffer.d.ts +8 -0
  55. package/dist/widgets/toDomProps.d.ts +26 -4
  56. package/dist/widgets/toHostGroupProps.d.ts +53 -0
  57. package/dist/widgets/toHostProps.d.ts +105 -0
  58. package/dist/widgets/types.d.ts +11 -3
  59. package/dist/widgets/upload-error-message.d.ts +6 -0
  60. package/dist/widgets/uploadingScope.d.ts +142 -0
  61. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  62. package/dist/widgets/useRecordQuery.d.ts +17 -2
  63. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  64. package/package.json +11 -12
@@ -0,0 +1,142 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * An ambient "is any upload still in flight?" scope for a whole form subtree.
4
+ *
5
+ * ## Why this exists beside `onUploadingChange`, and why it is not a second contract
6
+ *
7
+ * `onUploadingChange` (ADR-0059) is a PER-WIDGET prop: a host that renders one
8
+ * upload control and knows its name can pass a callback and keep the answer
9
+ * itself. That is `ActionParamDialog`, which holds a `Record<paramName, boolean>`
10
+ * because it lays out its own params one by one.
11
+ *
12
+ * A record form cannot do that. Its fields are rendered by the `form` node
13
+ * renderer from a `fields` array — the host hands over a list of field configs
14
+ * and never touches the widget — and its upload widgets may sit an arbitrary
15
+ * number of levels down (a section, a tab, a line-items subform). There is no
16
+ * place in that chain where the form host can attach a per-widget callback, so
17
+ * a record form was left with no notion of upload state at all and could submit
18
+ * mid-upload: the record was written WITHOUT the attachment and reported
19
+ * success (objectui#10166).
20
+ *
21
+ * The scope is the aggregation, not a second producer. `useUploadingSignal` is
22
+ * still the ONE place a widget's in-flight state is published; it drives the
23
+ * host prop and this scope from that same call, so the two can never disagree.
24
+ * A widget reports nothing extra and knows nothing about the scope, and a host
25
+ * that mounts no provider (every host on `main` today) is unaffected — the
26
+ * context read answers `null` and the reporting hook is inert.
27
+ *
28
+ * ## The one thing a mount-bound report cannot see (objectui#10180)
29
+ *
30
+ * A widget's in-flight state is React state, so it ends when the widget
31
+ * unmounts — and an upload does not. `FileField` does not abort its upload
32
+ * when it unmounts; the promise settles later and hands the value to the
33
+ * `onChange` it captured, which the form still accepts. So "the widget
34
+ * unmounted" is not "the upload ended". A wizard step the user leaves is the
35
+ * everyday case: leaving the step drops the widget's report, the wizard's final
36
+ * gate reads `false` while the upload is still running, and a Create pressed in
37
+ * that window writes the record WITHOUT the attachment and reports success.
38
+ *
39
+ * The in-repo upload widgets therefore also hold the scope for the lifetime of
40
+ * the upload ITSELF, through {@link useUploadingScopeHold}: taken when the
41
+ * upload starts, released when its promise settles, whether or not the widget
42
+ * is still mounted by then. While the widget is mounted the two entries start
43
+ * and stop together, so they cannot disagree; after it unmounts the hold is the
44
+ * only entry left, and it is the true one. The hold is internal to this package
45
+ * — it is not exported from the package entry.
46
+ */
47
+ /**
48
+ * The write half of a scope. Kept separate from the boolean so a widget that
49
+ * only REPORTS does not re-render when some other widget's upload flips —
50
+ * reporters read this context, the host owns the boolean.
51
+ */
52
+ interface UploadingScopeSink {
53
+ /**
54
+ * Record whether the widget registered under `id` is currently uploading.
55
+ * Reporting `false` for an unknown id is a no-op, which is what makes the
56
+ * unmount release below safe to call unconditionally.
57
+ */
58
+ report: (id: string, uploading: boolean) => void;
59
+ }
60
+ /** What {@link useUploadingScope} hands a host. */
61
+ export interface UploadingScope {
62
+ /** True while at least one upload widget inside the scope is in flight. */
63
+ anyUploading: boolean;
64
+ /**
65
+ * Passed to {@link UploadingScopeProvider}. Opaque to the host: it is the
66
+ * channel the widgets report on, not state the host is meant to read.
67
+ */
68
+ sink: UploadingScopeSink;
69
+ }
70
+ /**
71
+ * Own an uploading scope. The host reads `anyUploading` to gate its own submit
72
+ * and to say WHY that control is disabled, and renders its form subtree inside
73
+ * {@link UploadingScopeProvider}.
74
+ *
75
+ * The host owns the state rather than reading it back out of the context on
76
+ * purpose: a provider cannot consume its own value, and splitting it would give
77
+ * "is anything uploading" two authors.
78
+ */
79
+ export declare function useUploadingScope(): UploadingScope;
80
+ /**
81
+ * Publish a host's {@link useUploadingScope} to every upload widget rendered
82
+ * below it. Nesting is legal and CHAINS: an inner scope gates its own Save on
83
+ * its own uploads AND reports itself to the scope above, so an outer Save whose
84
+ * write encompasses the inner content is never blind to it. See
85
+ * {@link useUploadingScope} for why the direction is not a preference.
86
+ */
87
+ export declare function UploadingScopeProvider({ scope, children, }: {
88
+ scope: UploadingScope;
89
+ children: React.ReactNode;
90
+ }): React.ReactElement;
91
+ /**
92
+ * Report this widget's in-flight state to the enclosing scope, if there is one.
93
+ * Called by `useUploadingSignal` so every widget that already publishes to a
94
+ * host prop publishes here too, from the same state.
95
+ *
96
+ * ⚠️ The unmount release is load-bearing, not tidiness. A widget removed while
97
+ * its upload is in flight — a section collapsing, a tab switching, a subform
98
+ * row deleted — would otherwise leave its id in the set with nothing left to
99
+ * ever clear it, and the form's Save would stay disabled for the rest of the
100
+ * session. That failure mode is worse than the defect this scope fixes,
101
+ * because it has no user-visible cause at all.
102
+ *
103
+ * This report covers the widget's MOUNTED lifetime only. The upload's own
104
+ * lifetime, which can outlast the widget, is covered by
105
+ * {@link useUploadingScopeHold} — that is what keeps the release above from
106
+ * also releasing an upload that is still running (objectui#10180).
107
+ */
108
+ export declare function useUploadingScopeReport(uploading: boolean): void;
109
+ /**
110
+ * Hold the enclosing scope "uploading" for the lifetime of ONE upload
111
+ * (objectui#10180). The returned `hold()` is called when an upload starts and
112
+ * answers that upload's `release()`, to be called once its promise settles —
113
+ * in a `finally`, so a failed upload releases too.
114
+ *
115
+ * ## Why this is separate from the mount-bound report
116
+ *
117
+ * {@link useUploadingScopeReport} has to release on unmount (see there), and
118
+ * an upload keeps running after the widget that started it has unmounted —
119
+ * `FileField` does not abort it, and its settle still reaches the form's
120
+ * `onChange`. Leaving a wizard step mid-upload is exactly that: the step's
121
+ * widgets unmount, their reports are released, and without this hold the
122
+ * wizard's final gate read `false` while the file was still on its way, so a
123
+ * Create pressed in that window wrote the record without it.
124
+ *
125
+ * ## Why this cannot wedge the form
126
+ *
127
+ * The entry is released by the upload's own settle, not by a render, so it is
128
+ * cleared exactly when the upload ends, mounted widget or not. It can stay
129
+ * held only as long as the upload itself stays unsettled — the same bound a
130
+ * mounted widget's report already has — and while it is held the host shows
131
+ * the reason it gives for any upload in flight.
132
+ *
133
+ * The sink is captured when the upload STARTS: the scope the user picked the
134
+ * file under is the one whose save the file belongs to. A release that reaches
135
+ * a scope whose host has since unmounted is a no-op, and that host already
136
+ * released itself from any scope above it on the way out.
137
+ *
138
+ * Inert without a provider, like the report. Not exported from the package
139
+ * entry: it is how this package's own upload widgets report, not a contract.
140
+ */
141
+ export declare function useUploadingScopeHold(): () => () => void;
142
+ export {};
@@ -9,8 +9,9 @@ import { CascadingOptions, OptionLike, DependsOnInput } from '@object-ui/core';
9
9
  * empty the list is *gated* — callers surface a "select the parent first" hint
10
10
  * rather than an unfiltered set, mirroring the dependent-lookup UX.
11
11
  *
12
- * This is the React wrapper that sources `record` (the live form values) and the
13
- * predicate scope from context; the actual resolution is the pure
12
+ * This is the React wrapper: `record` (the live form values) arrives on the
13
+ * `dependentValues` argument its HOST passes, the predicate scope comes from
14
+ * context, and the actual resolution is the pure
14
15
  * {@link resolveCascadingOptions} in `@object-ui/core`, shared with the form
15
16
  * renderer so gating/filtering can never drift between them (#2715).
16
17
  */
@@ -12,7 +12,9 @@ import { DataSource } from '@object-ui/types';
12
12
  * (`{ data, total }`, tolerating a bare array),
13
13
  * - owns `records`/`loading`/`error`/`total` plus the `page`/`search`/`sort`
14
14
  * controls, with a debounced search path,
15
- * - clears itself when `enabled` goes false (e.g. a dialog closing).
15
+ * - clears itself when it can no longer query — `enabled` false (e.g. a dialog
16
+ * closing), or no data source or object name — superseding any read still in
17
+ * flight, so that read's answer commits nothing (objectui#10712).
16
18
  *
17
19
  * The caller keeps ownership of *selection* state and of record→option mapping;
18
20
  * this hook only answers "what records match the current query". It is the
@@ -43,7 +45,7 @@ export interface UseRecordQueryOptions {
43
45
  */
44
46
  paginate?: boolean;
45
47
  /**
46
- * `$filter` — already merged by the caller (base `lookup_filters`, dependent
48
+ * `$filter` — already merged by the caller (base `lookupFilters`, dependent
47
49
  * lookup chain, candidate hygiene like `banned != true`, …). Compared by
48
50
  * value, so a referentially-new-but-equal object each render will not loop.
49
51
  *
@@ -76,6 +78,19 @@ export interface UseRecordQueryResult {
76
78
  totalPages: number;
77
79
  page: number;
78
80
  search: string;
81
+ /**
82
+ * The search term `records` answer: the term of the query that committed
83
+ * them, `''` before the first answer and after a reset. It is set in the same
84
+ * commit as `records`, so the two never disagree.
85
+ *
86
+ * `search` moves on every keystroke, and the query for it runs after the
87
+ * debounce, so for that window `records` still answer an earlier term. A
88
+ * consumer that acts on "the rows for what the user typed" compares this
89
+ * with `search` (objectui#13162): equal means the rows on screen answer the
90
+ * term in the box. A late answer to a superseded query commits nothing
91
+ * (objectui#10712), so it cannot set this either.
92
+ */
93
+ answeredSearch: string;
79
94
  sort: RecordQuerySort | null;
80
95
  /** Jump to a page (1-indexed). No-op unless `paginate` is set. */
81
96
  setPage: (page: number) => void;
@@ -5,5 +5,13 @@
5
5
  * submitted mid-upload (the value only becomes the fileId once the upload
6
6
  * settles). Fires with the latest callback and only when `uploading` actually
7
7
  * changes, so an inline arrow prop doesn't thrash the parent every render.
8
+ *
9
+ * It ALSO reports into the ambient `UploadingScope` when one is mounted above
10
+ * the widget (objectui#10166). The prop suits a host that renders one named
11
+ * control; a record form hands its fields to the `form` node renderer and never
12
+ * touches the widget, so it has no place to attach a callback — the scope is how
13
+ * that host learns an upload is in flight. Both sinks are driven from this one
14
+ * call so they cannot disagree, and a host that mounts no provider is
15
+ * unaffected: `useUploadingScopeReport` is inert without one.
8
16
  */
9
17
  export declare function useUploadingSignal(uploading: boolean, onUploadingChange?: (uploading: boolean) => void): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/fields",
3
- "version": "17.6.0",
3
+ "version": "18.0.0",
4
4
  "description": "Field renderers and registry for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,19 +23,18 @@
23
23
  "LICENSE"
24
24
  ],
25
25
  "dependencies": {
26
- "@objectstack/spec": "^17.0.0",
27
- "clsx": "^2.1.1",
28
- "lucide-react": "^1.31.0",
26
+ "@objectstack/spec": "^18.0.0",
27
+ "lucide-react": "^1.43.0",
29
28
  "react-markdown": "^10.1.0",
30
29
  "rehype-sanitize": "^6.0.0",
31
30
  "remark-gfm": "^4.0.1",
32
- "tailwind-merge": "^3.6.0",
33
- "@object-ui/components": "17.6.0",
34
- "@object-ui/core": "17.6.0",
35
- "@object-ui/i18n": "17.6.0",
36
- "@object-ui/providers": "17.6.0",
37
- "@object-ui/react": "17.6.0",
38
- "@object-ui/types": "17.6.0"
31
+ "@object-ui/components": "18.0.0",
32
+ "@object-ui/core": "18.0.0",
33
+ "@object-ui/i18n": "18.0.0",
34
+ "@object-ui/permissions": "18.0.0",
35
+ "@object-ui/providers": "18.0.0",
36
+ "@object-ui/react": "18.0.0",
37
+ "@object-ui/types": "18.0.0"
39
38
  },
40
39
  "peerDependencies": {
41
40
  "react": "^18.0.0 || ^19.0.0",
@@ -83,7 +82,7 @@
83
82
  "build": "tsc && vite build && node scripts/build-css.mjs",
84
83
  "clean": "rm -rf dist",
85
84
  "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
86
- "test": "vitest run",
85
+ "test": "vitest run --root ../.. packages/fields/",
87
86
  "lint": "eslint ."
88
87
  }
89
88
  }