@object-ui/fields 17.5.0 → 17.7.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 (249) hide show
  1. package/CHANGELOG.md +6150 -0
  2. package/README.md +108 -8
  3. package/dist/FieldEditWidget.d.ts +70 -8
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/field-type-alias.d.ts +16 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +53 -11
  9. package/dist/index.d.ts +653 -128
  10. package/dist/index.js +4275 -3171
  11. package/dist/widgets/AddressField.d.ts +3 -3
  12. package/dist/widgets/AutoNumberField.d.ts +1 -1
  13. package/dist/widgets/AvatarField.d.ts +20 -4
  14. package/dist/widgets/BooleanField.d.ts +2 -2
  15. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  16. package/dist/widgets/CheckboxesField.d.ts +1 -1
  17. package/dist/widgets/CodeField.d.ts +1 -1
  18. package/dist/widgets/ColorField.d.ts +1 -1
  19. package/dist/widgets/CurrencyField.d.ts +1 -1
  20. package/dist/widgets/DateField.d.ts +2 -2
  21. package/dist/widgets/DateTimeField.d.ts +2 -2
  22. package/dist/widgets/EmailField.d.ts +1 -1
  23. package/dist/widgets/FileField.d.ts +18 -2
  24. package/dist/widgets/FilterConditionField.d.ts +43 -2
  25. package/dist/widgets/FormulaField.d.ts +1 -1
  26. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  27. package/dist/widgets/GeolocationField.d.ts +10 -4
  28. package/dist/widgets/GridField.d.ts +102 -86
  29. package/dist/widgets/ImageField.d.ts +1 -1
  30. package/dist/widgets/LocationField.d.ts +31 -4
  31. package/dist/widgets/LookupField.d.ts +9 -1
  32. package/dist/widgets/MasterDetailField.d.ts +1 -1
  33. package/dist/widgets/MultiSelectField.d.ts +1 -1
  34. package/dist/widgets/NumberField.d.ts +2 -2
  35. package/dist/widgets/ObjectField.d.ts +10 -3
  36. package/dist/widgets/ObjectRefField.d.ts +1 -1
  37. package/dist/widgets/OptionsEmptyState.d.ts +38 -8
  38. package/dist/widgets/PasswordField.d.ts +15 -1
  39. package/dist/widgets/PeoplePicker.d.ts +17 -2
  40. package/dist/widgets/PercentField.d.ts +3 -2
  41. package/dist/widgets/PersonRow.d.ts +13 -2
  42. package/dist/widgets/PhoneField.d.ts +1 -1
  43. package/dist/widgets/QRCodeField.d.ts +1 -1
  44. package/dist/widgets/RadioField.d.ts +1 -1
  45. package/dist/widgets/RatingField.d.ts +1 -1
  46. package/dist/widgets/RecipientPickerField.d.ts +44 -1
  47. package/dist/widgets/RecordPickerDialog.d.ts +33 -16
  48. package/dist/widgets/RichTextField.d.ts +37 -1
  49. package/dist/widgets/SelectField.d.ts +1 -1
  50. package/dist/widgets/SelectionTray.d.ts +13 -2
  51. package/dist/widgets/SignatureField.d.ts +32 -2
  52. package/dist/widgets/SliderField.d.ts +37 -2
  53. package/dist/widgets/SummaryField.d.ts +1 -1
  54. package/dist/widgets/TagsField.d.ts +1 -1
  55. package/dist/widgets/TextAreaField.d.ts +6 -2
  56. package/dist/widgets/TextField.d.ts +2 -2
  57. package/dist/widgets/TimeField.d.ts +2 -2
  58. package/dist/widgets/UrlField.d.ts +1 -1
  59. package/dist/widgets/UserField.d.ts +1 -1
  60. package/dist/widgets/VectorField.d.ts +1 -1
  61. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  62. package/dist/widgets/address-format.d.ts +27 -7
  63. package/dist/widgets/file-affordance.d.ts +33 -0
  64. package/dist/widgets/file-size-guard.d.ts +5 -23
  65. package/dist/widgets/file-value.d.ts +38 -9
  66. package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
  67. package/dist/widgets/nativeDateValue.d.ts +9 -27
  68. package/dist/widgets/number-format.d.ts +137 -0
  69. package/dist/widgets/numberBadInput.d.ts +135 -0
  70. package/dist/widgets/personDisplay.d.ts +22 -4
  71. package/dist/widgets/richTextDisplay.d.ts +116 -0
  72. package/dist/widgets/toDomProps.d.ts +10 -1
  73. package/dist/widgets/toHostGroupProps.d.ts +172 -21
  74. package/dist/widgets/toHostProps.d.ts +105 -0
  75. package/dist/widgets/types.d.ts +19 -6
  76. package/dist/widgets/upload-error-message.d.ts +6 -0
  77. package/dist/widgets/uploadingScope.d.ts +142 -0
  78. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  79. package/dist/widgets/useRecordQuery.d.ts +4 -2
  80. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  81. package/package.json +11 -13
  82. package/dist/widgets/CharacterCount.d.ts +0 -56
  83. package/src/FieldEditWidget.test.ts +0 -133
  84. package/src/FieldEditWidget.tsx +0 -207
  85. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  86. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  87. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  88. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  89. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  90. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  91. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  92. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  93. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  94. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  95. package/src/__tests__/cell-truncation.test.tsx +0 -96
  96. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  97. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  98. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  99. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  100. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  101. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  102. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  103. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  104. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  105. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  106. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  107. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  108. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  109. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  110. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  111. package/src/__tests__/registry-collision.test.ts +0 -91
  112. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  113. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  114. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  115. package/src/__tests__/validation-feedback.test.tsx +0 -361
  116. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  117. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  118. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  119. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  120. package/src/coerce-safe-value.test.ts +0 -34
  121. package/src/complex-widgets.test.tsx +0 -924
  122. package/src/currency.ts +0 -144
  123. package/src/datetime-widgets.test.tsx +0 -185
  124. package/src/field-type-alias.multiple.test.ts +0 -81
  125. package/src/field-type-alias.ts +0 -156
  126. package/src/field-type-coverage.test.ts +0 -67
  127. package/src/index.css +0 -44
  128. package/src/index.tsx +0 -2745
  129. package/src/standard-widgets.test.tsx +0 -364
  130. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  131. package/src/widgets/AddressField.tsx +0 -182
  132. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  133. package/src/widgets/AutoNumberField.tsx +0 -18
  134. package/src/widgets/AvatarField.tsx +0 -117
  135. package/src/widgets/BooleanField.tsx +0 -86
  136. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  137. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  138. package/src/widgets/CharacterCount.tsx +0 -263
  139. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  140. package/src/widgets/CheckboxesField.tsx +0 -169
  141. package/src/widgets/CodeField.tsx +0 -41
  142. package/src/widgets/ColorField.tsx +0 -53
  143. package/src/widgets/CurrencyField.tsx +0 -135
  144. package/src/widgets/DateField.tsx +0 -40
  145. package/src/widgets/DateTimeField.tsx +0 -46
  146. package/src/widgets/EmailField.tsx +0 -37
  147. package/src/widgets/FileField.tsx +0 -512
  148. package/src/widgets/FilterConditionField.tsx +0 -454
  149. package/src/widgets/FormulaField.tsx +0 -38
  150. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  151. package/src/widgets/GeolocationField.tsx +0 -193
  152. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  153. package/src/widgets/GridField.test.tsx +0 -632
  154. package/src/widgets/GridField.tsx +0 -1070
  155. package/src/widgets/ImageCropperDialog.tsx +0 -245
  156. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  157. package/src/widgets/ImageField.tsx +0 -305
  158. package/src/widgets/ImageLightbox.test.tsx +0 -52
  159. package/src/widgets/ImageLightbox.tsx +0 -108
  160. package/src/widgets/LocationField.tsx +0 -57
  161. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  162. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  163. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  164. package/src/widgets/LookupField.idField.test.tsx +0 -61
  165. package/src/widgets/LookupField.tsx +0 -1295
  166. package/src/widgets/MarkdownContent.tsx +0 -19
  167. package/src/widgets/MasterDetailField.tsx +0 -131
  168. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  169. package/src/widgets/MultiSelectField.tsx +0 -196
  170. package/src/widgets/NumberField.tsx +0 -49
  171. package/src/widgets/ObjectField.tsx +0 -94
  172. package/src/widgets/ObjectRefField.tsx +0 -121
  173. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  174. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  175. package/src/widgets/OptionsEmptyState.tsx +0 -99
  176. package/src/widgets/PasswordField.tsx +0 -49
  177. package/src/widgets/PeoplePicker.test.tsx +0 -360
  178. package/src/widgets/PeoplePicker.tsx +0 -579
  179. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  180. package/src/widgets/PercentField.tsx +0 -95
  181. package/src/widgets/PersonRow.tsx +0 -117
  182. package/src/widgets/PhoneField.tsx +0 -37
  183. package/src/widgets/QRCodeField.tsx +0 -103
  184. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  185. package/src/widgets/RadioField.tsx +0 -143
  186. package/src/widgets/RatingField.tsx +0 -94
  187. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  188. package/src/widgets/RecipientPickerField.tsx +0 -188
  189. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  190. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  191. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  192. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  193. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  194. package/src/widgets/RichTextField.tsx +0 -176
  195. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  196. package/src/widgets/SelectField.tsx +0 -193
  197. package/src/widgets/SelectionTray.tsx +0 -110
  198. package/src/widgets/SignatureField.tsx +0 -140
  199. package/src/widgets/SliderField.tsx +0 -39
  200. package/src/widgets/SummaryField.tsx +0 -31
  201. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  202. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  203. package/src/widgets/TagsField.tsx +0 -88
  204. package/src/widgets/TextAreaField.tsx +0 -213
  205. package/src/widgets/TextField.tsx +0 -45
  206. package/src/widgets/TimeField.tsx +0 -31
  207. package/src/widgets/UrlField.tsx +0 -46
  208. package/src/widgets/UserField.tsx +0 -59
  209. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  210. package/src/widgets/VectorField.tsx +0 -29
  211. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  212. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  213. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  214. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  215. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  216. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  217. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  218. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  219. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  220. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  221. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  222. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  223. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  224. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  225. package/src/widgets/address-format.ts +0 -120
  226. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  227. package/src/widgets/deriveLookupColumns.ts +0 -108
  228. package/src/widgets/file-size-guard.ts +0 -55
  229. package/src/widgets/file-value.test.ts +0 -230
  230. package/src/widgets/file-value.ts +0 -246
  231. package/src/widgets/nativeDateValue.test.ts +0 -115
  232. package/src/widgets/nativeDateValue.ts +0 -112
  233. package/src/widgets/openNativePicker.ts +0 -18
  234. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  235. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  236. package/src/widgets/personDisplay.test.ts +0 -113
  237. package/src/widgets/personDisplay.ts +0 -92
  238. package/src/widgets/recentLookups.test.ts +0 -45
  239. package/src/widgets/recentLookups.ts +0 -47
  240. package/src/widgets/toDomProps.ts +0 -204
  241. package/src/widgets/toHostGroupProps.ts +0 -102
  242. package/src/widgets/types.ts +0 -242
  243. package/src/widgets/useCascadingOptions.ts +0 -46
  244. package/src/widgets/useFieldTranslation.ts +0 -132
  245. package/src/widgets/useRecordQuery.test.ts +0 -174
  246. package/src/widgets/useRecordQuery.ts +0 -291
  247. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  248. package/src/widgets/useUploadingSignal.ts +0 -20
  249. package/src/withFieldCarrier.tsx +0 -75
@@ -1,454 +0,0 @@
1
- import React from 'react';
2
- import { FilterBuilder, cn } from '@object-ui/components';
3
- import { SchemaRendererContext } from '@object-ui/react';
4
- import type { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { useFieldTranslation } from './useFieldTranslation';
7
-
8
- /**
9
- * FilterConditionField — visual criteria builder for a stored FilterCondition
10
- * (e.g. `sys_sharing_rule.criteria_json`), scoped to the object chosen in a
11
- * sibling `object_name` field.
12
- *
13
- * Reached via the field `widget: 'filter-condition'` hint (resolves as
14
- * `field:filter-condition`). Reads the live `object_name` from
15
- * `dependentValues`, loads that object's fields via
16
- * `dataSource.getObjectSchema(...)`, and renders `<FilterBuilder>` over them —
17
- * so an admin builds `type == "customer" AND is_active == true` by picking
18
- * fields/operators instead of hand-writing JSON.
19
- *
20
- * Storage contract: the value round-trips as a **MongoDB-style object filter**
21
- * (`{ field: value }`, `{ field: { $gt: n } }`, `{ $or: [...] }`), JSON-encoded
22
- * — the exact shape the sharing evaluator spreads into `engine.find(object,
23
- * { filter })`. Criteria that can't be represented in the builder (nested
24
- * mixes, unknown operators) fall back to a raw-JSON editor so nothing is hidden
25
- * or lost; an "Edit as JSON" toggle is always available.
26
- */
27
-
28
- interface FilterFieldDef {
29
- value: string;
30
- label: string;
31
- type?: string;
32
- options?: Array<{ value: string; label: string }>;
33
- referenceTo?: string;
34
- }
35
-
36
- interface BuilderCondition {
37
- id: string;
38
- field: string;
39
- operator: string;
40
- value: any;
41
- }
42
- interface BuilderGroup {
43
- id: string;
44
- logic: 'and' | 'or';
45
- conditions: BuilderCondition[];
46
- }
47
-
48
- const EMPTY_GROUP: BuilderGroup = { id: 'root', logic: 'and', conditions: [] };
49
-
50
- /** Field types that are not meaningfully filterable in a simple builder. */
51
- const NON_FILTERABLE = new Set([
52
- 'object', 'vector', 'file', 'image', 'avatar', 'signature',
53
- 'richtext', 'html', 'markdown', 'location', 'grid', 'json', 'code',
54
- ]);
55
-
56
- function deriveFilterFields(schema: any): FilterFieldDef[] {
57
- const raw = schema?.fields;
58
- const entries: Array<[string, any]> = Array.isArray(raw)
59
- ? raw.map((f: any) => [f?.name, f])
60
- : raw && typeof raw === 'object'
61
- ? Object.entries(raw)
62
- : [];
63
- const out: FilterFieldDef[] = [];
64
- for (const [name, f] of entries) {
65
- if (!name || !f || f.hidden) continue;
66
- const type = f.type as string | undefined;
67
- if (type && NON_FILTERABLE.has(type)) continue;
68
- out.push({
69
- value: name,
70
- label: f.label || name,
71
- type,
72
- options: Array.isArray(f.options)
73
- ? f.options.map((o: any) =>
74
- typeof o === 'string'
75
- ? { value: o, label: o }
76
- : { value: String(o?.value), label: String(o?.label ?? o?.value) },
77
- )
78
- : undefined,
79
- referenceTo: f.reference_to || f.reference,
80
- });
81
- }
82
- return out;
83
- }
84
-
85
- function coerceByType(value: any, type?: string): any {
86
- if (value == null) return value;
87
- if (type === 'boolean' || type === 'toggle') {
88
- if (typeof value === 'boolean') return value;
89
- if (value === 'true') return true;
90
- if (value === 'false') return false;
91
- return value;
92
- }
93
- if (type === 'number' || type === 'currency' || type === 'percent' || type === 'rating' || type === 'slider') {
94
- const n = Number(value);
95
- return Number.isFinite(n) && value !== '' ? n : value;
96
- }
97
- return value;
98
- }
99
-
100
- function toArray(value: any): any[] {
101
- if (Array.isArray(value)) return value;
102
- if (typeof value === 'string') return value.split(',').map((s) => s.trim()).filter(Boolean);
103
- return value == null ? [] : [value];
104
- }
105
-
106
- /**
107
- * Builder condition → `$`-operator criteria. Exported for tests: this is the
108
- * chokepoint where a builder token becomes a spec `FieldOperatorsSchema` key,
109
- * and a wrong spelling here is rejected downstream by `convertFiltersToAST`
110
- * rather than at authoring time. @internal
111
- */
112
- export function condToMongo(c: BuilderCondition, typeOf: (f: string) => string | undefined): Record<string, any> | null {
113
- const { field, operator, value } = c || ({} as BuilderCondition);
114
- if (!field) return null;
115
- const t = typeOf(field);
116
- const cv = coerceByType(value, t);
117
- switch (operator) {
118
- case 'equals': return { [field]: cv };
119
- case 'notEquals': return { [field]: { $ne: cv } };
120
- case 'contains': return { [field]: { $contains: value } };
121
- // `$notContains` is the spec spelling (FieldOperatorsSchema, data/filter.zod.ts).
122
- // This emitted `$ncontains` — a token that appears nowhere in @objectstack/spec and
123
- // that convertFiltersToAST throws on, so every "does not contain" rule authored here
124
- // was rejected downstream. See kvToCondition for reading the old spelling back.
125
- case 'notContains': return { [field]: { $notContains: value } };
126
- // String-specific spec operators — previously unreachable from the
127
- // builder UI even though FieldOperatorsSchema accepts them (#2942).
128
- case 'startsWith': return { [field]: { $startsWith: value } };
129
- case 'endsWith': return { [field]: { $endsWith: value } };
130
- case 'isEmpty': return { [field]: { $in: [null, ''] } };
131
- case 'isNotEmpty': return { [field]: { $nin: [null, ''] } };
132
- // Null / existence spec operators. Distinct from isEmpty/isNotEmpty,
133
- // which also treat '' as empty.
134
- case 'isNull': return { [field]: { $null: true } };
135
- case 'isNotNull': return { [field]: { $null: false } };
136
- case 'exists': return { [field]: { $exists: true } };
137
- case 'notExists': return { [field]: { $exists: false } };
138
- case 'greaterThan':
139
- case 'after': return { [field]: { $gt: cv } };
140
- case 'lessThan':
141
- case 'before': return { [field]: { $lt: cv } };
142
- case 'greaterOrEqual': return { [field]: { $gte: cv } };
143
- case 'lessOrEqual': return { [field]: { $lte: cv } };
144
- case 'between': {
145
- const [a, b] = Array.isArray(value) ? value : [undefined, undefined];
146
- return { [field]: { $gte: coerceByType(a, t), $lte: coerceByType(b, t) } };
147
- }
148
- case 'in': return { [field]: { $in: toArray(value).map((v) => coerceByType(v, t)) } };
149
- case 'notIn': return { [field]: { $nin: toArray(value).map((v) => coerceByType(v, t)) } };
150
- default: return { [field]: cv };
151
- }
152
- }
153
-
154
- function filterGroupToMongo(group: BuilderGroup, typeOf: (f: string) => string | undefined): Record<string, any> | null {
155
- const frags = (group?.conditions ?? [])
156
- .map((c) => condToMongo(c, typeOf))
157
- .filter((x): x is Record<string, any> => !!x);
158
- // No conditions → no predicate. NOT "match all": a sharing rule with an
159
- // empty criteria is refused on save, and one already stored shares nothing
160
- // (objectstack#3896 / ADR-0049). `null` becomes an empty stored value, which
161
- // is what the required-criteria messaging below keys off.
162
- if (frags.length === 0) return null;
163
- if (group.logic === 'or') return { $or: frags };
164
- if (frags.length === 1) return frags[0];
165
- const keys = frags.flatMap((f) => Object.keys(f));
166
- const noCollision = new Set(keys).size === keys.length;
167
- return noCollision ? Object.assign({}, ...frags) : { $and: frags };
168
- }
169
-
170
- function arraysEqual(a: any, b: any[]): boolean {
171
- return Array.isArray(a) && a.length === b.length && a.every((v, i) => v === b[i]);
172
- }
173
-
174
- /**
175
- * Criteria → builder condition (the reverse of {@link condToMongo}). Returning
176
- * `null` makes the builder refuse to load the rule ("criteria can't be
177
- * represented"), so this must keep accepting spellings previously written.
178
- * @internal
179
- */
180
- export function kvToCondition(field: string, v: any, idx: number): BuilderCondition | null {
181
- const id = `c_${idx}_${field}`;
182
- if (v === null || typeof v !== 'object' || Array.isArray(v)) {
183
- return { id, field, operator: 'equals', value: v };
184
- }
185
- const opKeys = Object.keys(v);
186
- if (opKeys.length === 1) {
187
- const op = opKeys[0];
188
- const val = v[op];
189
- switch (op) {
190
- case '$ne': return { id, field, operator: 'notEquals', value: val };
191
- case '$contains': return { id, field, operator: 'contains', value: val };
192
- // `$ncontains` is the pre-fix spelling this widget used to emit. Criteria saved
193
- // before the fix still carry it, so keep reading it — dropping it here would make
194
- // those rules fail to load ("criteria can't be represented") instead of migrating.
195
- case '$notContains':
196
- case '$ncontains': return { id, field, operator: 'notContains', value: val };
197
- case '$gt': return { id, field, operator: 'greaterThan', value: val };
198
- case '$lt': return { id, field, operator: 'lessThan', value: val };
199
- case '$gte': return { id, field, operator: 'greaterOrEqual', value: val };
200
- case '$lte': return { id, field, operator: 'lessOrEqual', value: val };
201
- case '$startsWith': return { id, field, operator: 'startsWith', value: val };
202
- case '$endsWith': return { id, field, operator: 'endsWith', value: val };
203
- case '$null': return { id, field, operator: val === false ? 'isNotNull' : 'isNull', value: '' };
204
- case '$exists': return { id, field, operator: val === false ? 'notExists' : 'exists', value: '' };
205
- case '$in':
206
- return arraysEqual(val, [null, ''])
207
- ? { id, field, operator: 'isEmpty', value: '' }
208
- : { id, field, operator: 'in', value: val };
209
- case '$nin':
210
- return arraysEqual(val, [null, ''])
211
- ? { id, field, operator: 'isNotEmpty', value: '' }
212
- : { id, field, operator: 'notIn', value: val };
213
- default: return null;
214
- }
215
- }
216
- if (opKeys.length === 2 && '$gte' in v && '$lte' in v) {
217
- return { id, field, operator: 'between', value: [v.$gte, v.$lte] };
218
- }
219
- return null;
220
- }
221
-
222
- /** Returns a BuilderGroup, or `null` when the criteria can't be represented. */
223
- function mongoToFilterGroup(mongo: any): BuilderGroup | null {
224
- if (mongo == null) return { ...EMPTY_GROUP, conditions: [] };
225
- if (typeof mongo !== 'object' || Array.isArray(mongo)) return null;
226
- const entries = Object.entries(mongo);
227
- if (entries.length === 0) return { ...EMPTY_GROUP, conditions: [] };
228
- if (entries.length === 1 && (mongo.$or || mongo.$and)) {
229
- const logic: 'and' | 'or' = mongo.$or ? 'or' : 'and';
230
- const arr = mongo.$or || mongo.$and;
231
- if (!Array.isArray(arr)) return null;
232
- const conditions: BuilderCondition[] = [];
233
- for (let i = 0; i < arr.length; i++) {
234
- const frag = arr[i];
235
- if (!frag || typeof frag !== 'object' || Object.keys(frag).length !== 1) return null;
236
- const field = Object.keys(frag)[0];
237
- if (field.startsWith('$')) return null;
238
- const c = kvToCondition(field, frag[field], i);
239
- if (!c) return null;
240
- conditions.push(c);
241
- }
242
- return { id: 'root', logic, conditions };
243
- }
244
- const conditions: BuilderCondition[] = [];
245
- let i = 0;
246
- for (const [field, v] of entries) {
247
- if (field.startsWith('$')) return null; // mixed logical + field → raw
248
- const c = kvToCondition(field, v, i++);
249
- if (!c) return null;
250
- conditions.push(c);
251
- }
252
- return { id: 'root', logic: 'and', conditions };
253
- }
254
-
255
- /**
256
- * Would this criteria select EVERY record of the object?
257
- *
258
- * Mirrors the server's `isMatchAllCriteria` (objectstack `plugin-sharing`,
259
- * #3896) closely enough to warn before the round-trip: blank, `{}`, `[]`, and
260
- * the vacuous combinators. Deliberately conservative in the same direction —
261
- * the cost of a false positive is one extra hint, the cost of a false negative
262
- * is a save that fails with a toast. The server stays authoritative.
263
- *
264
- * @internal exported for tests
265
- */
266
- export function isMatchAllCriteria(parsed: any): boolean {
267
- if (parsed == null) return true;
268
- if (Array.isArray(parsed)) return parsed.every(isMatchAllCriteria);
269
- if (typeof parsed !== 'object') return true;
270
- const entries = Object.entries(parsed);
271
- if (entries.length === 0) return true;
272
- for (const [key, value] of entries) {
273
- if (key === '$and') {
274
- if (!Array.isArray(value) || value.every(isMatchAllCriteria)) continue;
275
- return false;
276
- }
277
- if (key === '$or') {
278
- if (!Array.isArray(value) || value.length === 0 || value.some(isMatchAllCriteria)) continue;
279
- return false;
280
- }
281
- return false;
282
- }
283
- return true;
284
- }
285
-
286
- function stringifyValue(value: string | object | undefined | null): string {
287
- if (value == null || value === '') return '';
288
- if (typeof value === 'string') return value;
289
- try {
290
- return JSON.stringify(value);
291
- } catch {
292
- return '';
293
- }
294
- }
295
-
296
- export function FilterConditionField({
297
- value,
298
- onChange,
299
- readonly,
300
- className,
301
- error,
302
- ...props
303
- }: FieldWidgetComponentProps<string | object>) {
304
- const ctx = React.useContext(SchemaRendererContext);
305
- const { t } = useFieldTranslation();
306
- const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null;
307
- const dependentValues: Record<string, any> = (props as any).dependentValues ?? {};
308
- const objectName = String(dependentValues.object_name ?? '');
309
-
310
- const [fields, setFields] = React.useState<FilterFieldDef[] | null>(null);
311
-
312
- React.useEffect(() => {
313
- setFields(null);
314
- if (!dataSource || !objectName || typeof dataSource.getObjectSchema !== 'function') return;
315
- let cancelled = false;
316
- (async () => {
317
- try {
318
- const schema = await dataSource.getObjectSchema(objectName);
319
- if (!cancelled) setFields(deriveFilterFields(schema));
320
- } catch {
321
- if (!cancelled) setFields([]);
322
- }
323
- })();
324
- return () => {
325
- cancelled = true;
326
- };
327
- }, [dataSource, objectName]);
328
-
329
- const rawValue = React.useMemo(() => stringifyValue(value), [value]);
330
-
331
- const parsed = React.useMemo(() => {
332
- if (!rawValue.trim()) return { mongo: {}, ok: true };
333
- try {
334
- return { mongo: JSON.parse(rawValue), ok: true };
335
- } catch {
336
- return { mongo: null, ok: false };
337
- }
338
- }, [rawValue]);
339
-
340
- const group = React.useMemo(
341
- () => (parsed.ok ? mongoToFilterGroup(parsed.mongo) : null),
342
- [parsed],
343
- );
344
-
345
- // Only flag a criteria that PARSES to match-all. Unparsable JSON has its own
346
- // message (`invalidJson`) and must not collect a second, contradictory one.
347
- const isEmptyCriteria = React.useMemo(
348
- () => parsed.ok && isMatchAllCriteria(parsed.mongo),
349
- [parsed],
350
- );
351
-
352
- // Raw JSON mode: forced when the stored value can't be represented in the
353
- // builder; otherwise opt-in via the toggle.
354
- const representable = parsed.ok && group !== null;
355
- const [rawMode, setRawMode] = React.useState<boolean>(!representable);
356
- React.useEffect(() => {
357
- if (!representable) setRawMode(true);
358
- }, [representable]);
359
-
360
- const typeOf = React.useMemo(() => {
361
- const map = new Map((fields ?? []).map((f) => [f.value, f.type]));
362
- return (f: string) => map.get(f);
363
- }, [fields]);
364
-
365
- const handleBuilderChange = (g: BuilderGroup) => {
366
- const mongo = filterGroupToMongo(g, typeOf);
367
- onChange((mongo == null ? '' : JSON.stringify(mongo)) as any);
368
- };
369
-
370
- if (!objectName) {
371
- return (
372
- <p className={cn('text-sm text-muted-foreground', className)}>
373
- {t('fields.filterCondition.selectObjectFirst')}
374
- </p>
375
- );
376
- }
377
-
378
- if (readonly) {
379
- if (!rawValue.trim()) {
380
- // Used to read "All records" — which was both wrong and the most
381
- // dangerous thing this widget could say (objectstack#3896). A rule with
382
- // no criteria has never usefully shared everything; it now shares
383
- // nothing and is refused on save, so name that instead.
384
- return (
385
- <span className={cn('text-sm text-destructive', className)}>
386
- {t('fields.filterCondition.noCriteria')}
387
- </span>
388
- );
389
- }
390
- return (
391
- <pre className={cn('overflow-x-auto rounded bg-muted/40 p-2 text-xs', className)}>
392
- {rawValue}
393
- </pre>
394
- );
395
- }
396
-
397
- return (
398
- <div className={cn('flex flex-col gap-2', className)}>
399
- {rawMode ? (
400
- <>
401
- <textarea
402
- // DOM pass-through onto the raw-JSON editing surface
403
- // (objectui#3318). NOTE this widget stays on the #3318 ledger
404
- // regardless: its dependency-gated state (no `object_name` chosen
405
- // yet — the state a fresh form and the registry sweep render) is a
406
- // plain hint paragraph with no focusable control.
407
- {...toDomProps(props)}
408
- className="min-h-[96px] w-full rounded border bg-background px-2 py-1 font-mono text-xs"
409
- value={rawValue}
410
- placeholder='{ "type": "customer", "is_active": true }'
411
- onChange={(e) => onChange(e.target.value as any)}
412
- // The form's validation slot (#3222) OR this widget's own
413
- // unparsable-JSON state, which already renders its red message.
414
- aria-invalid={!!error || !parsed.ok}
415
- />
416
- {!parsed.ok && (
417
- <span className="text-xs text-destructive">{t('fields.filterCondition.invalidJson')}</span>
418
- )}
419
- </>
420
- ) : (
421
- <FilterBuilder
422
- fields={fields ?? []}
423
- value={(group ?? EMPTY_GROUP) as any}
424
- onChange={handleBuilderChange as any}
425
- />
426
- )}
427
- {/*
428
- The server refuses to save a criteria that would select every record
429
- (objectstack#3896), but that rejection only arrives as a toast after
430
- the admin hits Save. Say it here, while they are still looking at the
431
- empty builder — and never imply that leaving it empty means "share
432
- everything", which is what this widget used to do.
433
- */}
434
- {isEmptyCriteria && (
435
- <span className="text-xs text-destructive">
436
- {t('fields.filterCondition.criteriaRequired')}
437
- </span>
438
- )}
439
- <button
440
- type="button"
441
- className="self-start text-xs text-muted-foreground underline-offset-2 hover:underline"
442
- onClick={() => setRawMode((m) => !m)}
443
- disabled={!representable && !rawMode}
444
- title={!representable ? t('fields.filterCondition.jsonOnly') : undefined}
445
- >
446
- {rawMode
447
- ? t('fields.filterCondition.useVisualBuilder')
448
- : t('fields.filterCondition.editAsJson')}
449
- </button>
450
- </div>
451
- );
452
- }
453
-
454
- export default FilterConditionField;
@@ -1,38 +0,0 @@
1
- import React from 'react';
2
- import { EmptyValue } from '@object-ui/components';
3
- import { useDisplayLocale } from '@object-ui/i18n';
4
- import { FieldWidgetComponentProps } from './types';
5
-
6
- /**
7
- * FormulaField - Read-only computed field
8
- * Values are computed on the backend and cannot be edited
9
- */
10
- export function FormulaField({ value, field, ...props }: FieldWidgetComponentProps<any>) {
11
- // Before the empty-value early return — the hook count must not change when
12
- // a value flips between null and set. A `date` return type used to format
13
- // through the MACHINE's locale (objectui#4468).
14
- const locale = useDisplayLocale();
15
- const formulaField = field as any;
16
- const returnType = formulaField?.return_type || 'text';
17
-
18
- if (value == null) {
19
- return <EmptyValue className={props.className} />;
20
- }
21
-
22
- let displayValue: string;
23
- if (returnType === 'number' || returnType === 'currency') {
24
- displayValue = typeof value === 'number' ? value.toFixed(2) : String(value);
25
- } else if (returnType === 'boolean') {
26
- displayValue = value ? 'Yes' : 'No';
27
- } else if (returnType === 'date') {
28
- displayValue = new Date(value).toLocaleDateString(locale);
29
- } else {
30
- displayValue = String(value);
31
- }
32
-
33
- return (
34
- <span className={`text-sm font-mono text-gray-700 ${props.className || ''}`}>
35
- {displayValue}
36
- </span>
37
- );
38
- }
@@ -1,60 +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 { FullscreenEditor, type FullscreenEditorProps } from '@object-ui/components';
11
-
12
- /**
13
- * The fullscreen-edit affordance shared by every long-text widget in this
14
- * package (`field:textarea` → `TextAreaField`, `field:markdown` / `field:html`
15
- * → `RichTextField`).
16
- *
17
- * ## This is now a thin wrapper (objectui#3398)
18
- *
19
- * The affordance, the dialog, the draft/commit state machine and the
20
- * `readOnly` / `disabled` semantics all live in `FullscreenEditor`
21
- * (`@object-ui/components`). They used to live here AND, separately, in the
22
- * form renderer's built-in `textarea` branch — one form-level promise
23
- * (`ObjectFormSchema.mobile.fullscreenLongText`, projected onto every long-text
24
- * field as `mobile_fullscreen`) answered twice, on the two render paths.
25
- *
26
- * Two copies of a state machine drift, and these did: objectui#3400 measured a
27
- * read-only field editable through the built-in branch's dialog, objectui#3402
28
- * measured the same hole for `disabled` here, and #3393 (label in the dialog
29
- * title) and #3272 (i18n) each landed on one side before the other. The
30
- * maintainer's ruling of 2026-08-10 was one implementation hoisted to the
31
- * dependency-legal side — and the measured import graph allows exactly that
32
- * direction: this package depends on `@object-ui/components`, which declares no
33
- * dependency on `@object-ui/fields` in `dependencies` or `peerDependencies`.
34
- *
35
- * ## Why the wrapper survives the merge rather than being deleted
36
- *
37
- * It carries the one thing that is genuinely about THIS package: `readOnly` is
38
- * not part of the contract here. Both hosts early-return a read-only DISPLAY
39
- * before they compute the affordance (`TextAreaField.tsx`, `RichTextField.tsx`),
40
- * so a `readOnly` reaching this component would be a prop with no producer on
41
- * this path — the shape this package keeps deleting (objectui#3232/#3233). The
42
- * `Omit` below is that statement in the type system, and it is why the hoist did
43
- * not simply re-export the primitive. A future host that renders an editor for
44
- * read-only fields must widen this type AND pass the prop together; the
45
- * primitive already answers it (no affordance at all).
46
- *
47
- * Everything else — `value` / `onCommit` / `label` / `testIdPrefix` /
48
- * `disabled` / `children` / `footer` — passes straight through, so the hosts and
49
- * their pins are unchanged. `testIdPrefix` still namespaces this package's ids
50
- * (`textarea-`, `richtext-`), distinct from the built-in branch's
51
- * `form-textarea-`, so a test can still say which render path it found.
52
- */
53
- export type FullscreenFieldEditorProps = Omit<FullscreenEditorProps, 'readOnly'>;
54
-
55
- export function FullscreenFieldEditor(props: FullscreenFieldEditorProps) {
56
- // `readOnly` is deliberately not forwarded — it is not in the props type, and
57
- // the primitive defaults it to false, which is the behaviour both hosts rely
58
- // on: they have already returned a read-only display before reaching here.
59
- return <FullscreenEditor {...props} />;
60
- }