@object-ui/fields 17.3.0 → 17.6.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 (265) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +1 -12
  3. package/dist/FieldEditWidget.d.ts +23 -3
  4. package/dist/currency.d.ts +46 -0
  5. package/dist/field-type-alias.d.ts +35 -1
  6. package/dist/index.cjs +4 -4
  7. package/dist/index.css +651 -0
  8. package/dist/index.d.ts +474 -85
  9. package/dist/index.js +5418 -5023
  10. package/dist/widgets/AddressField.d.ts +4 -12
  11. package/dist/widgets/AutoNumberField.d.ts +1 -1
  12. package/dist/widgets/AvatarField.d.ts +1 -1
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  15. package/dist/widgets/CheckboxesField.d.ts +1 -1
  16. package/dist/widgets/CodeField.d.ts +1 -1
  17. package/dist/widgets/ColorField.d.ts +1 -1
  18. package/dist/widgets/CurrencyField.d.ts +1 -1
  19. package/dist/widgets/DateField.d.ts +1 -1
  20. package/dist/widgets/DateTimeField.d.ts +1 -1
  21. package/dist/widgets/EmailField.d.ts +1 -1
  22. package/dist/widgets/FileField.d.ts +18 -2
  23. package/dist/widgets/FilterConditionField.d.ts +28 -1
  24. package/dist/widgets/FormulaField.d.ts +1 -1
  25. package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
  26. package/dist/widgets/GeolocationField.d.ts +1 -1
  27. package/dist/widgets/GridField.d.ts +68 -6
  28. package/dist/widgets/ImageField.d.ts +1 -1
  29. package/dist/widgets/LocationField.d.ts +1 -1
  30. package/dist/widgets/LookupField.d.ts +1 -1
  31. package/dist/widgets/MasterDetailField.d.ts +1 -1
  32. package/dist/widgets/MultiSelectField.d.ts +1 -1
  33. package/dist/widgets/NumberField.d.ts +1 -1
  34. package/dist/widgets/ObjectField.d.ts +1 -1
  35. package/dist/widgets/ObjectRefField.d.ts +1 -1
  36. package/dist/widgets/OptionsEmptyState.d.ts +21 -1
  37. package/dist/widgets/PasswordField.d.ts +1 -1
  38. package/dist/widgets/PercentField.d.ts +1 -1
  39. package/dist/widgets/PhoneField.d.ts +1 -1
  40. package/dist/widgets/QRCodeField.d.ts +1 -1
  41. package/dist/widgets/RadioField.d.ts +1 -1
  42. package/dist/widgets/RatingField.d.ts +1 -1
  43. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  44. package/dist/widgets/RecordPickerDialog.d.ts +34 -10
  45. package/dist/widgets/RichTextField.d.ts +37 -1
  46. package/dist/widgets/SelectField.d.ts +26 -6
  47. package/dist/widgets/SignatureField.d.ts +32 -2
  48. package/dist/widgets/SliderField.d.ts +37 -2
  49. package/dist/widgets/SummaryField.d.ts +1 -1
  50. package/dist/widgets/TagsField.d.ts +1 -1
  51. package/dist/widgets/TextAreaField.d.ts +6 -2
  52. package/dist/widgets/TextField.d.ts +1 -1
  53. package/dist/widgets/TimeField.d.ts +1 -1
  54. package/dist/widgets/UrlField.d.ts +1 -1
  55. package/dist/widgets/UserField.d.ts +1 -1
  56. package/dist/widgets/VectorField.d.ts +1 -1
  57. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  58. package/dist/widgets/address-format.d.ts +88 -0
  59. package/dist/widgets/file-size-guard.d.ts +41 -0
  60. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  61. package/dist/widgets/nativeDateValue.d.ts +37 -0
  62. package/dist/widgets/richTextDisplay.d.ts +80 -0
  63. package/dist/widgets/toDomProps.d.ts +30 -10
  64. package/dist/widgets/toHostGroupProps.d.ts +189 -0
  65. package/dist/widgets/types.d.ts +8 -3
  66. package/dist/widgets/useRecordQuery.d.ts +7 -1
  67. package/package.json +17 -14
  68. package/dist/FieldEditWidget.test.d.ts +0 -8
  69. package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
  70. package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
  71. package/dist/__tests__/cell-truncation.test.d.ts +0 -0
  72. package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
  73. package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
  74. package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
  75. package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
  76. package/dist/__tests__/registry-collision.test.d.ts +0 -0
  77. package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
  78. package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
  79. package/dist/__tests__/validation-feedback.test.d.ts +0 -0
  80. package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
  81. package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
  82. package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
  83. package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
  84. package/dist/coerce-safe-value.test.d.ts +0 -1
  85. package/dist/complex-widgets.test.d.ts +0 -1
  86. package/dist/datetime-widgets.test.d.ts +0 -1
  87. package/dist/field-type-coverage.test.d.ts +0 -1
  88. package/dist/standard-widgets.test.d.ts +0 -0
  89. package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
  90. package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
  91. package/dist/widgets/CharacterCount.d.ts +0 -56
  92. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  93. package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
  94. package/dist/widgets/GridField.test.d.ts +0 -1
  95. package/dist/widgets/ImageLightbox.test.d.ts +0 -1
  96. package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
  97. package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
  98. package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
  99. package/dist/widgets/LookupField.idField.test.d.ts +0 -1
  100. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  101. package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
  102. package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
  103. package/dist/widgets/PeoplePicker.test.d.ts +0 -8
  104. package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
  105. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  106. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  107. package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
  108. package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
  109. package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
  110. package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
  111. package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
  112. package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
  113. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
  114. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
  115. package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
  116. package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
  117. package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
  118. package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
  119. package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
  120. package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
  121. package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
  122. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
  123. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
  124. package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
  125. package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
  126. package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
  127. package/dist/widgets/file-value.test.d.ts +0 -1
  128. package/dist/widgets/personDisplay.test.d.ts +0 -8
  129. package/dist/widgets/recentLookups.test.d.ts +0 -1
  130. package/dist/widgets/useRecordQuery.test.d.ts +0 -8
  131. package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
  132. package/src/FieldEditWidget.test.ts +0 -133
  133. package/src/FieldEditWidget.tsx +0 -191
  134. package/src/__tests__/DateCellRenderer.test.tsx +0 -73
  135. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  136. package/src/__tests__/cell-truncation.test.tsx +0 -96
  137. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  138. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  139. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  140. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  141. package/src/__tests__/registry-collision.test.ts +0 -91
  142. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  143. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  144. package/src/__tests__/validation-feedback.test.tsx +0 -361
  145. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  146. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  147. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
  148. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  149. package/src/coerce-safe-value.test.ts +0 -34
  150. package/src/complex-widgets.test.tsx +0 -908
  151. package/src/currency.ts +0 -14
  152. package/src/datetime-widgets.test.tsx +0 -93
  153. package/src/field-type-alias.ts +0 -100
  154. package/src/field-type-coverage.test.ts +0 -67
  155. package/src/index.tsx +0 -2503
  156. package/src/standard-widgets.test.tsx +0 -357
  157. package/src/widgets/AddressField.tsx +0 -132
  158. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
  159. package/src/widgets/AutoNumberField.tsx +0 -18
  160. package/src/widgets/AvatarField.tsx +0 -117
  161. package/src/widgets/BooleanField.tsx +0 -52
  162. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  163. package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
  164. package/src/widgets/CharacterCount.tsx +0 -263
  165. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  166. package/src/widgets/CheckboxesField.tsx +0 -136
  167. package/src/widgets/CodeField.tsx +0 -41
  168. package/src/widgets/ColorField.tsx +0 -53
  169. package/src/widgets/CurrencyField.tsx +0 -95
  170. package/src/widgets/DateField.tsx +0 -31
  171. package/src/widgets/DateTimeField.tsx +0 -37
  172. package/src/widgets/EmailField.tsx +0 -37
  173. package/src/widgets/FileField.tsx +0 -494
  174. package/src/widgets/FilterConditionField.tsx +0 -454
  175. package/src/widgets/FormulaField.tsx +0 -33
  176. package/src/widgets/FullscreenFieldEditor.tsx +0 -271
  177. package/src/widgets/GeolocationField.tsx +0 -167
  178. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  179. package/src/widgets/GridField.test.tsx +0 -406
  180. package/src/widgets/GridField.tsx +0 -966
  181. package/src/widgets/ImageCropperDialog.tsx +0 -245
  182. package/src/widgets/ImageField.tsx +0 -255
  183. package/src/widgets/ImageLightbox.test.tsx +0 -52
  184. package/src/widgets/ImageLightbox.tsx +0 -108
  185. package/src/widgets/LocationField.tsx +0 -57
  186. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  187. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  188. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  189. package/src/widgets/LookupField.idField.test.tsx +0 -61
  190. package/src/widgets/LookupField.tsx +0 -1291
  191. package/src/widgets/MarkdownContent.tsx +0 -19
  192. package/src/widgets/MasterDetailField.tsx +0 -131
  193. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  194. package/src/widgets/MultiSelectField.tsx +0 -142
  195. package/src/widgets/NumberField.tsx +0 -49
  196. package/src/widgets/ObjectField.tsx +0 -94
  197. package/src/widgets/ObjectRefField.tsx +0 -121
  198. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  199. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  200. package/src/widgets/OptionsEmptyState.tsx +0 -83
  201. package/src/widgets/PasswordField.tsx +0 -49
  202. package/src/widgets/PeoplePicker.test.tsx +0 -360
  203. package/src/widgets/PeoplePicker.tsx +0 -579
  204. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  205. package/src/widgets/PercentField.tsx +0 -95
  206. package/src/widgets/PersonRow.tsx +0 -117
  207. package/src/widgets/PhoneField.tsx +0 -37
  208. package/src/widgets/QRCodeField.tsx +0 -103
  209. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  210. package/src/widgets/RadioField.tsx +0 -111
  211. package/src/widgets/RatingField.tsx +0 -81
  212. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  213. package/src/widgets/RecipientPickerField.tsx +0 -188
  214. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  215. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  216. package/src/widgets/RecordPickerDialog.tsx +0 -1316
  217. package/src/widgets/RichTextField.tsx +0 -176
  218. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  219. package/src/widgets/SelectField.tsx +0 -162
  220. package/src/widgets/SelectionTray.tsx +0 -110
  221. package/src/widgets/SignatureField.tsx +0 -140
  222. package/src/widgets/SliderField.tsx +0 -39
  223. package/src/widgets/SummaryField.tsx +0 -31
  224. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  225. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  226. package/src/widgets/TagsField.tsx +0 -88
  227. package/src/widgets/TextAreaField.tsx +0 -213
  228. package/src/widgets/TextField.tsx +0 -45
  229. package/src/widgets/TimeField.tsx +0 -31
  230. package/src/widgets/UrlField.tsx +0 -46
  231. package/src/widgets/UserField.tsx +0 -58
  232. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  233. package/src/widgets/VectorField.tsx +0 -29
  234. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  235. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
  236. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  237. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  238. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  239. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  240. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  241. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  242. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  243. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  244. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  245. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  246. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  247. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  248. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  249. package/src/widgets/deriveLookupColumns.ts +0 -108
  250. package/src/widgets/file-value.test.ts +0 -230
  251. package/src/widgets/file-value.ts +0 -246
  252. package/src/widgets/openNativePicker.ts +0 -18
  253. package/src/widgets/personDisplay.test.ts +0 -113
  254. package/src/widgets/personDisplay.ts +0 -92
  255. package/src/widgets/recentLookups.test.ts +0 -45
  256. package/src/widgets/recentLookups.ts +0 -47
  257. package/src/widgets/toDomProps.ts +0 -175
  258. package/src/widgets/types.ts +0 -242
  259. package/src/widgets/useCascadingOptions.ts +0 -46
  260. package/src/widgets/useFieldTranslation.ts +0 -150
  261. package/src/widgets/useRecordQuery.test.ts +0 -174
  262. package/src/widgets/useRecordQuery.ts +0 -264
  263. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  264. package/src/widgets/useUploadingSignal.ts +0 -20
  265. package/src/withFieldCarrier.tsx +0 -75
@@ -1,176 +0,0 @@
1
- import React from 'react';
2
- import { cn, Textarea, EmptyValue } from '@object-ui/components';
3
- import { useObjectTranslation } from '@object-ui/react';
4
- import { FullscreenFieldEditor } from './FullscreenFieldEditor';
5
- import { FieldWidgetComponentProps } from './types';
6
-
7
- /**
8
- * The rich-text editing surface, rendered by `RichTextField` in BOTH positions:
9
- * inline in the form, and inside the fullscreen dialog.
10
- *
11
- * Sharing it is the point of objectui#3301's acceptance criterion "the dialog
12
- * holds the real editor". Had the dialog inlined a bare `<Textarea>` instead,
13
- * the two surfaces would already disagree (no format indicator) and would keep
14
- * disagreeing every time this widget gains an affordance — whatever the editor
15
- * grows into (a toolbar, a TipTap/Lexical instance, a preview toggle), both
16
- * positions get it at once because there is only one of them.
17
- *
18
- * `fullHeight` is the only difference between the two renderings: inline the
19
- * textarea is `rows`-sized, in the dialog it fills the available height.
20
- */
21
- function RichTextEditorSurface({
22
- value,
23
- onChange,
24
- formatLabel,
25
- hint,
26
- placeholder,
27
- rows,
28
- disabled,
29
- error,
30
- className,
31
- fullHeight,
32
- autoFocus,
33
- textareaTestId,
34
- overlay,
35
- }: {
36
- value: string;
37
- onChange: (next: string) => void;
38
- formatLabel: string;
39
- hint: string;
40
- placeholder?: string;
41
- rows?: number;
42
- disabled?: boolean;
43
- error?: string;
44
- className?: string;
45
- fullHeight?: boolean;
46
- autoFocus?: boolean;
47
- textareaTestId?: string;
48
- /** Absolutely-positioned children over the textarea (the expand affordance). */
49
- overlay?: React.ReactNode;
50
- }) {
51
- return (
52
- <div className={cn('space-y-2', fullHeight && 'flex flex-col h-full')}>
53
- <div className="flex items-center justify-between text-xs text-gray-500">
54
- <span>{formatLabel}</span>
55
- <span className="italic">{hint}</span>
56
- </div>
57
- <div className={cn('relative', fullHeight && 'flex-1 min-h-0')}>
58
- <Textarea
59
- value={value || ''}
60
- onChange={(e) => onChange(e.target.value)}
61
- placeholder={placeholder}
62
- disabled={disabled}
63
- autoFocus={autoFocus}
64
- rows={fullHeight ? undefined : rows}
65
- // `text-base` in the dialog for the same reason `TextAreaField` uses
66
- // it there: sub-16px inputs make iOS Safari zoom on focus, which is
67
- // exactly wrong for a surface the user opened to get more room.
68
- className={cn(
69
- 'font-mono',
70
- fullHeight ? 'text-base h-full min-h-full resize-none' : 'text-sm',
71
- className,
72
- )}
73
- aria-invalid={!!error}
74
- data-testid={textareaTestId}
75
- />
76
- {overlay}
77
- </div>
78
- </div>
79
- );
80
- }
81
-
82
- /**
83
- * Rich text field with markdown/HTML support
84
- * For now, this is a simple textarea. A full implementation would use
85
- * a rich text editor like TipTap, Lexical, or Slate.
86
- *
87
- * Reached by forms as `field:markdown` and `field:html`; both resolve here.
88
- *
89
- * ## Fullscreen editing (objectui#3301)
90
- *
91
- * `ObjectForm` stamps `mobile_fullscreen` onto the metadata of every long-text
92
- * field — textarea AND rich-text — when `ObjectFormSchema.mobile
93
- * .fullscreenLongText` is set, and `ObjectFormSchema.mobile`'s own JSDoc has
94
- * always promised "textarea/rich-text get an expand button". This widget never
95
- * read the flag, so for `field:markdown` / `field:html` that promise did
96
- * nothing at all: the producer stamped, and no consumer existed.
97
- *
98
- * The flag is read off `field` and nowhere else — the single metadata carrier
99
- * since objectui#3233 — matching `TextAreaField` read for read, so a
100
- * misspelled flag stays inert in both widgets rather than being caught by a
101
- * tolerant fallback in one of them. The affordance and dialog themselves come
102
- * from the shared `FullscreenFieldEditor`, so one form-level setting keeps
103
- * producing one behaviour across both widgets.
104
- */
105
- export function RichTextField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
106
- const { t } = useObjectTranslation();
107
- if (readonly) {
108
- return (
109
- <div
110
- className="text-sm prose prose-sm max-w-none"
111
- >
112
- {value || <EmptyValue />}
113
- </div>
114
- );
115
- }
116
-
117
- const richField = field as any;
118
- const rows = richField?.rows || 8;
119
- const format = richField?.format || 'markdown'; // 'markdown' or 'html'
120
- // Same single read as `TextAreaField`: the field metadata is the only
121
- // carrier, and this widget is the second consumer the flag always had.
122
- const showFullscreenButton = Boolean(richField?.mobile_fullscreen);
123
- // Resolved once and given to BOTH renderings of the editor (objectui#3402) —
124
- // exactly like `formatLabel` / `hint` / `placeholder` below, and for the same
125
- // reason. Landing it on the inline surface alone left a disabled rich-text
126
- // field greyed out next to a live expand button whose dialog committed any
127
- // edit through `onCommit`. `disabled` also carries the form's `isSubmitting`.
128
- const disabled = Boolean(props.disabled);
129
-
130
- // Resolved once and handed to BOTH renderings of the editor, so the dialog
131
- // cannot drift into showing different copy than the inline surface.
132
- const formatLabel = t('fields.richText.format', { format, defaultValue: `Format: ${format}` });
133
- const hint = t('fields.richText.basicEditorHint', { defaultValue: 'Rich text editor (basic)' });
134
- const placeholder =
135
- richField?.placeholder ||
136
- t('fields.richText.placeholder', { defaultValue: 'Enter text...' });
137
-
138
- return (
139
- <RichTextEditorSurface
140
- value={value}
141
- onChange={onChange}
142
- formatLabel={formatLabel}
143
- hint={hint}
144
- placeholder={placeholder}
145
- rows={rows}
146
- disabled={readonly || disabled}
147
- error={error}
148
- className={props.className}
149
- overlay={
150
- showFullscreenButton && (
151
- <FullscreenFieldEditor
152
- value={value ?? ''}
153
- onCommit={onChange}
154
- label={richField?.label}
155
- testIdPrefix="richtext"
156
- disabled={disabled}
157
- >
158
- {(draft, setDraft, editorDisabled) => (
159
- <RichTextEditorSurface
160
- value={draft}
161
- onChange={setDraft}
162
- formatLabel={formatLabel}
163
- hint={hint}
164
- placeholder={placeholder}
165
- disabled={editorDisabled}
166
- autoFocus
167
- fullHeight
168
- textareaTestId="richtext-fullscreen-input"
169
- />
170
- )}
171
- </FullscreenFieldEditor>
172
- )
173
- }
174
- />
175
- );
176
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Cascading / role-gated `select` options (#2284).
11
- *
12
- * Exercises the observable, non-Radix behaviour of {@link SelectField}: the
13
- * dependency gate (a "select the parent first" empty-state) and the cascade
14
- * clear (a value dropped when the offered set no longer includes it). The
15
- * per-option filtering itself is unit-tested in `@object-ui/core`
16
- * (`optionRules.test.ts`); here we prove the widget wires `dependentValues` +
17
- * predicate scope into that resolver.
18
- */
19
- import { describe, it, expect, vi, beforeEach } from 'vitest';
20
- import React from 'react';
21
- import { render, screen } from '@testing-library/react';
22
- import '@testing-library/jest-dom';
23
- import { PredicateScopeProvider } from '@object-ui/react';
24
- import { SelectField } from './SelectField';
25
-
26
- const provinceField = {
27
- name: 'province',
28
- type: 'select',
29
- dependsOn: 'country',
30
- options: [
31
- { label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" },
32
- { label: 'California', value: 'ca', visibleWhen: "record.country == 'us'" },
33
- ],
34
- } as any;
35
-
36
- beforeEach(() => {
37
- vi.clearAllMocks();
38
- });
39
-
40
- describe('SelectField — dependency gating (#2284)', () => {
41
- it('gates with a "select parent first" hint while the controlling field is empty', () => {
42
- render(
43
- <SelectField
44
- value={undefined}
45
- onChange={vi.fn()}
46
- field={provinceField}
47
- {...({ name: 'province', dependentValues: {} } as any)}
48
- />,
49
- );
50
- const gate = screen.getByTestId('select-empty-province');
51
- expect(gate).toHaveTextContent(/select country first/i);
52
- // Gated → no live combobox is offered.
53
- expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
54
- });
55
-
56
- it('unlocks the combobox once the controlling field is set', () => {
57
- render(
58
- <SelectField
59
- value={undefined}
60
- onChange={vi.fn()}
61
- field={provinceField}
62
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
63
- />,
64
- );
65
- expect(screen.queryByTestId('select-empty-province')).not.toBeInTheDocument();
66
- expect(screen.getByRole('combobox')).toBeInTheDocument();
67
- });
68
- });
69
-
70
- describe('SelectField — cascade clear (#2284)', () => {
71
- it('clears a value the parent change no longer offers', () => {
72
- const onChange = vi.fn();
73
- render(
74
- <SelectField
75
- value={'ca'}
76
- onChange={onChange}
77
- field={provinceField}
78
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
79
- />,
80
- );
81
- // 'ca' is a US province — under country=cn it is not offered, so it is dropped.
82
- expect(onChange).toHaveBeenCalledWith(undefined);
83
- });
84
-
85
- it('keeps a value that is still offered', () => {
86
- const onChange = vi.fn();
87
- render(
88
- <SelectField
89
- value={'zj'}
90
- onChange={onChange}
91
- field={provinceField}
92
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
93
- />,
94
- );
95
- expect(onChange).not.toHaveBeenCalled();
96
- });
97
- });
98
-
99
- describe('SelectField — role / context gating (#2284)', () => {
100
- const tierField = {
101
- name: 'tier',
102
- type: 'select',
103
- options: [
104
- { label: 'Standard', value: 'standard' },
105
- { label: 'Admin only', value: 'admin_only', visibleWhen: "'admin' in current_user.positions" },
106
- ],
107
- } as any;
108
-
109
- it('clears an admin-only value for a non-admin (offered set excludes it)', () => {
110
- const onChange = vi.fn();
111
- render(
112
- <PredicateScopeProvider scope={{ current_user: { positions: ['sales'] } }}>
113
- <SelectField
114
- value={'admin_only'}
115
- onChange={onChange}
116
- field={tierField}
117
- {...({ name: 'tier', dependentValues: {} } as any)}
118
- />
119
- </PredicateScopeProvider>,
120
- );
121
- expect(onChange).toHaveBeenCalledWith(undefined);
122
- });
123
-
124
- it('keeps an admin-only value for an admin', () => {
125
- const onChange = vi.fn();
126
- render(
127
- <PredicateScopeProvider scope={{ current_user: { positions: ['admin'] } }}>
128
- <SelectField
129
- value={'admin_only'}
130
- onChange={onChange}
131
- field={tierField}
132
- {...({ name: 'tier', dependentValues: {} } as any)}
133
- />
134
- </PredicateScopeProvider>,
135
- );
136
- expect(onChange).not.toHaveBeenCalled();
137
- });
138
- });
@@ -1,162 +0,0 @@
1
- import React, { useEffect } from 'react';
2
- import {
3
- Select,
4
- SelectContent,
5
- SelectItem,
6
- SelectTrigger,
7
- SelectValue,
8
- EmptyValue,
9
- } from '@object-ui/components';
10
- import { isValueStillOffered } from '@object-ui/core';
11
- import { SelectFieldMetadata } from '@object-ui/types';
12
- import { useFieldTranslation } from './useFieldTranslation';
13
- import { FieldWidgetComponentProps } from './types';
14
- import { toDomProps } from './toDomProps';
15
- import { MultiSelectField } from './MultiSelectField';
16
- import { OptionsEmptyState } from './OptionsEmptyState';
17
- import { useCascadingOptions } from './useCascadingOptions';
18
-
19
- /**
20
- * SelectField - dropdown selection widget.
21
- *
22
- * A field declared `multiple: true` selects zero-or-more values (spec:
23
- * `multiple` is valid on `select`), so it renders the multi-value chip picker
24
- * — the same widget the `multiselect` type uses. Delegating here (rather than
25
- * only at a type-resolution layer) means every surface that renders the
26
- * `select` widget — the object form, the inline grid editor, and
27
- * `ActionParamDialog` — inherits multi-select identically, with no drift
28
- * between them. Single-value selects keep the cascading dropdown below.
29
- *
30
- * Both branches resolve per-option `visibleWhen` cascading / role-gating through
31
- * the shared {@link useCascadingOptions} hook (#2715), so single and multi stay
32
- * in lockstep.
33
- */
34
- export function SelectField(props: FieldWidgetComponentProps<any>) {
35
- const config = props.field as SelectFieldMetadata | undefined;
36
- if ((config as any)?.multiple) {
37
- // NOT `toDomProps` — this is a widget-to-widget delegation, not a DOM
38
- // spread. `MultiSelectField` implements the same contract and needs the
39
- // whole of it (`value`, `onChange`, `field`, `dataSource`, …); narrowing
40
- // here to the DOM whitelist would hand it an empty widget.
41
- return <MultiSelectField {...props} />;
42
- }
43
- return <SingleSelectField {...(props as FieldWidgetComponentProps<string>)} />;
44
- }
45
-
46
- /**
47
- * SingleSelectField - single-value dropdown with configurable options.
48
- *
49
- * Supports cascading / role-gated options (#2284): each option may carry a
50
- * `visibleWhen` CEL predicate, evaluated against the live form record +
51
- * `current_user`, so the offered set narrows as a controlling field changes
52
- * (`record.country == 'cn'`) or by role (`'admin' in current_user.positions`). A
53
- * field declares which sibling fields drive its list via `dependsOn`; while any
54
- * of those is empty the control is gated with a "select the parent first" hint,
55
- * mirroring the dependent-lookup UX.
56
- */
57
- function SingleSelectField({
58
- value,
59
- onChange,
60
- field,
61
- readonly,
62
- error,
63
- dependentValues,
64
- dependsOn: dependsOnProp,
65
- emptyHint,
66
- dataSource: _dataSource,
67
- ...props
68
- }: FieldWidgetComponentProps<string>) {
69
- const config = field as SelectFieldMetadata;
70
- const rawOptions = config?.options || [];
71
- const { t } = useFieldTranslation();
72
- // Stable hook for automation/e2e — react-hook-form + Radix Select cannot be
73
- // driven by synthetic DOM events, so e2e must target the trigger/options by a
74
- // deterministic testid keyed on the field name. `props.name` is the
75
- // react-hook-form field name spread in by the form renderer (FormField).
76
- const fieldName = props.name || (config as any)?.name || props.id || '';
77
-
78
- const dependsOn = (config as any)?.dependsOn ?? dependsOnProp;
79
- const { options, gated, dependsOnFields } = useCascadingOptions(
80
- rawOptions,
81
- dependsOn,
82
- dependentValues,
83
- );
84
-
85
- // Cascade clear: once the offered set no longer includes the current value
86
- // (parent changed / predicate flipped), drop it so no stale pair persists.
87
- useEffect(() => {
88
- if (readonly) return;
89
- if (value === undefined || value === null || (value as unknown) === '') return;
90
- if (!isValueStillOffered(value, options)) onChange?.(undefined as unknown as string);
91
- // eslint-disable-next-line react-hooks/exhaustive-deps
92
- }, [options, gated]);
93
-
94
- if (readonly) {
95
- const option = rawOptions.find((o) => o.value === value);
96
- const display = option?.label || value;
97
- return display ? <span className="text-sm">{display}</span> : <EmptyValue />;
98
- }
99
-
100
- // A select with no options is unfillable — a silently-empty Radix dropdown
101
- // reads as "broken widget" and hides the real cause. Surface a legible state:
102
- // the host's `emptyHint` when it computed one (it knows the controlling
103
- // fields' LABELS), else this widget's own translated copy. Shared with the
104
- // other option widgets so the four cannot drift again (objectui#3231).
105
- if (options.length === 0) {
106
- return (
107
- <OptionsEmptyState
108
- emptyHint={emptyHint}
109
- gated={gated}
110
- dependsOnFields={dependsOnFields}
111
- testId={fieldName ? `select-empty-${fieldName}` : undefined}
112
- className="h-9"
113
- />
114
- );
115
- }
116
-
117
- // Radix `Select.Root` renders no DOM element of its own — anything it does
118
- // not recognise (every `aria-*` / `data-*` a host hands this widget) is
119
- // silently DROPPED, never reaching a real element. That is how a required
120
- // select failed validation with the red message on screen while assistive
121
- // tech was told nothing (objectui#3306): the form renderer's `aria-invalid`
122
- // / `aria-describedby` / `aria-required` landed on Root and vanished. The
123
- // DOM pass-through therefore goes to `SelectTrigger` — the focusable
124
- // `<button role="combobox">` a user and their screen reader actually
125
- // interact with — with two deliberate exceptions kept on Root:
126
- //
127
- // - `name`: the ONE whitelist key Root genuinely consumes — it forwards it
128
- // to the hidden native `<select>` that takes part in form submission.
129
- // On the trigger it would sit uselessly on a non-submitter `<button>`.
130
- // - `disabled`: Root is the single authority (it disables trigger, item
131
- // interaction and the hidden select together); forwarding the raw prop to
132
- // the trigger as well would give the state a second, OR-merged author.
133
- const { name: domName, disabled: _domDisabled, ...triggerDomProps } = toDomProps(props);
134
-
135
- return (
136
- <Select
137
- name={domName}
138
- value={value}
139
- onValueChange={onChange}
140
- disabled={readonly || props.disabled}
141
- >
142
- <SelectTrigger
143
- {...triggerDomProps}
144
- data-testid={fieldName ? `select-trigger-${fieldName}` : undefined}
145
- // AFTER the spread so this widget's own computation wins, the #3222
146
- // discipline: `error` is the published validation slot
147
- // (`FieldWidgetPropsSchema`), and `!!undefined` must yield an explicit
148
- // `"false"` — a valid field SAYS it is valid rather than staying mute.
149
- aria-invalid={!!error}
150
- >
151
- <SelectValue placeholder={config?.placeholder || t('common.selectOption')} />
152
- </SelectTrigger>
153
- <SelectContent position="popper">
154
- {options.map((option) => (
155
- <SelectItem key={option.value} value={option.value} data-testid={`select-option-${option.value}`}>
156
- {option.label}
157
- </SelectItem>
158
- ))}
159
- </SelectContent>
160
- </Select>
161
- );
162
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import React from 'react';
10
- import { Avatar, AvatarFallback, AvatarImage, cn } from '@object-ui/components';
11
- import { X } from 'lucide-react';
12
- import {
13
- getPersonName,
14
- getPersonInitials,
15
- getPersonAvatarUrl,
16
- getPersonId,
17
- } from './personDisplay';
18
-
19
- /**
20
- * "Selected" transfer-box area (穿梭框式已选区): live echo of the current
21
- * multi-selection as removable avatar chips. Reusable kernel — the future
22
- * org-tree tier composes the same tray beside its right-hand list. Purely
23
- * presentational: it takes the resolved records + an onRemove callback, and
24
- * accepts already-translated `label`/`emptyText` so it stays i18n-agnostic.
25
- */
26
- export interface SelectionTrayProps {
27
- /** Full record objects for the current selection. */
28
- records: any[];
29
- /** Remove a selected record by its id. */
30
- onRemove: (id: any) => void;
31
- displayField?: string;
32
- avatarField?: string;
33
- idField?: string;
34
- /** Header label, e.g. "Selected (3)" — omit to hide the header. */
35
- label?: string;
36
- /** When provided, a "Clear all" action shows in the header while non-empty. */
37
- onClear?: () => void;
38
- /** Localized "Clear all" text (i18n-agnostic — caller passes it). */
39
- clearLabel?: string;
40
- /** Shown when the selection is empty. */
41
- emptyText?: string;
42
- className?: string;
43
- }
44
-
45
- export function SelectionTray({
46
- records,
47
- onRemove,
48
- displayField = 'name',
49
- avatarField = 'image',
50
- idField = 'id',
51
- label,
52
- onClear,
53
- clearLabel,
54
- emptyText,
55
- className,
56
- }: SelectionTrayProps) {
57
- const showHeader = !!label || (!!onClear && records.length > 0);
58
- return (
59
- <div className={cn('flex flex-col gap-1.5', className)} data-testid="selection-tray">
60
- {showHeader && (
61
- <div className="flex items-center justify-between">
62
- {label && <div className="text-xs font-medium text-muted-foreground">{label}</div>}
63
- {onClear && records.length > 0 && (
64
- <button
65
- type="button"
66
- onClick={onClear}
67
- className="text-xs text-muted-foreground transition-colors hover:text-foreground"
68
- data-testid="selection-clear"
69
- >
70
- {clearLabel ?? 'Clear all'}
71
- </button>
72
- )}
73
- </div>
74
- )}
75
- {records.length === 0 ? (
76
- <div className="text-xs text-muted-foreground">{emptyText ?? ''}</div>
77
- ) : (
78
- <div className="flex flex-wrap gap-1.5">
79
- {records.map(record => {
80
- const id = getPersonId(record, idField);
81
- const name = getPersonName(record, displayField);
82
- const avatarUrl = getPersonAvatarUrl(record, avatarField);
83
- const initials = getPersonInitials(name);
84
- return (
85
- <span
86
- key={String(id)}
87
- data-testid="selection-chip"
88
- className="inline-flex items-center gap-1.5 rounded-full border bg-background py-0.5 pl-0.5 pr-1.5 text-sm"
89
- >
90
- <Avatar className="size-6 shrink-0">
91
- {avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
92
- <AvatarFallback className="text-[10px]">{initials}</AvatarFallback>
93
- </Avatar>
94
- <span className="max-w-[10rem] truncate">{name || '—'}</span>
95
- <button
96
- type="button"
97
- onClick={() => onRemove(id)}
98
- aria-label={`Remove ${name}`}
99
- className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
100
- >
101
- <X className="size-3.5" aria-hidden />
102
- </button>
103
- </span>
104
- );
105
- })}
106
- </div>
107
- )}
108
- </div>
109
- );
110
- }