@object-ui/fields 17.5.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 (229) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/dist/FieldEditWidget.d.ts +16 -2
  3. package/dist/field-type-alias.d.ts +16 -0
  4. package/dist/index.cjs +4 -4
  5. package/dist/index.css +27 -9
  6. package/dist/index.d.ts +425 -72
  7. package/dist/index.js +2530 -2228
  8. package/dist/widgets/AddressField.d.ts +3 -3
  9. package/dist/widgets/AutoNumberField.d.ts +1 -1
  10. package/dist/widgets/AvatarField.d.ts +1 -1
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/DateField.d.ts +1 -1
  18. package/dist/widgets/DateTimeField.d.ts +1 -1
  19. package/dist/widgets/EmailField.d.ts +1 -1
  20. package/dist/widgets/FileField.d.ts +18 -2
  21. package/dist/widgets/FilterConditionField.d.ts +28 -1
  22. package/dist/widgets/FormulaField.d.ts +1 -1
  23. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  24. package/dist/widgets/GeolocationField.d.ts +1 -1
  25. package/dist/widgets/GridField.d.ts +56 -4
  26. package/dist/widgets/ImageField.d.ts +1 -1
  27. package/dist/widgets/LocationField.d.ts +1 -1
  28. package/dist/widgets/LookupField.d.ts +1 -1
  29. package/dist/widgets/MasterDetailField.d.ts +1 -1
  30. package/dist/widgets/MultiSelectField.d.ts +1 -1
  31. package/dist/widgets/NumberField.d.ts +1 -1
  32. package/dist/widgets/ObjectField.d.ts +1 -1
  33. package/dist/widgets/ObjectRefField.d.ts +1 -1
  34. package/dist/widgets/OptionsEmptyState.d.ts +13 -7
  35. package/dist/widgets/PasswordField.d.ts +1 -1
  36. package/dist/widgets/PercentField.d.ts +1 -1
  37. package/dist/widgets/PhoneField.d.ts +1 -1
  38. package/dist/widgets/QRCodeField.d.ts +1 -1
  39. package/dist/widgets/RadioField.d.ts +1 -1
  40. package/dist/widgets/RatingField.d.ts +1 -1
  41. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  42. package/dist/widgets/RecordPickerDialog.d.ts +2 -4
  43. package/dist/widgets/RichTextField.d.ts +37 -1
  44. package/dist/widgets/SelectField.d.ts +1 -1
  45. package/dist/widgets/SignatureField.d.ts +32 -2
  46. package/dist/widgets/SliderField.d.ts +37 -2
  47. package/dist/widgets/SummaryField.d.ts +1 -1
  48. package/dist/widgets/TagsField.d.ts +1 -1
  49. package/dist/widgets/TextAreaField.d.ts +6 -2
  50. package/dist/widgets/TextField.d.ts +1 -1
  51. package/dist/widgets/TimeField.d.ts +1 -1
  52. package/dist/widgets/UrlField.d.ts +1 -1
  53. package/dist/widgets/UserField.d.ts +1 -1
  54. package/dist/widgets/VectorField.d.ts +1 -1
  55. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  56. package/dist/widgets/address-format.d.ts +27 -7
  57. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  58. package/dist/widgets/richTextDisplay.d.ts +80 -0
  59. package/dist/widgets/toHostGroupProps.d.ts +119 -21
  60. package/dist/widgets/types.d.ts +8 -3
  61. package/package.json +9 -10
  62. package/dist/widgets/CharacterCount.d.ts +0 -56
  63. package/src/FieldEditWidget.test.ts +0 -133
  64. package/src/FieldEditWidget.tsx +0 -207
  65. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  66. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  67. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  68. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  69. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  70. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  71. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  72. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  73. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  74. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  75. package/src/__tests__/cell-truncation.test.tsx +0 -96
  76. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  77. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  78. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  79. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  80. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  81. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  82. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  83. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  84. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  85. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  86. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  87. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  88. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  89. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  90. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  91. package/src/__tests__/registry-collision.test.ts +0 -91
  92. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  93. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  94. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  95. package/src/__tests__/validation-feedback.test.tsx +0 -361
  96. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  97. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  98. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  99. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  100. package/src/coerce-safe-value.test.ts +0 -34
  101. package/src/complex-widgets.test.tsx +0 -924
  102. package/src/currency.ts +0 -144
  103. package/src/datetime-widgets.test.tsx +0 -185
  104. package/src/field-type-alias.multiple.test.ts +0 -81
  105. package/src/field-type-alias.ts +0 -156
  106. package/src/field-type-coverage.test.ts +0 -67
  107. package/src/index.css +0 -44
  108. package/src/index.tsx +0 -2745
  109. package/src/standard-widgets.test.tsx +0 -364
  110. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  111. package/src/widgets/AddressField.tsx +0 -182
  112. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  113. package/src/widgets/AutoNumberField.tsx +0 -18
  114. package/src/widgets/AvatarField.tsx +0 -117
  115. package/src/widgets/BooleanField.tsx +0 -86
  116. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  117. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  118. package/src/widgets/CharacterCount.tsx +0 -263
  119. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  120. package/src/widgets/CheckboxesField.tsx +0 -169
  121. package/src/widgets/CodeField.tsx +0 -41
  122. package/src/widgets/ColorField.tsx +0 -53
  123. package/src/widgets/CurrencyField.tsx +0 -135
  124. package/src/widgets/DateField.tsx +0 -40
  125. package/src/widgets/DateTimeField.tsx +0 -46
  126. package/src/widgets/EmailField.tsx +0 -37
  127. package/src/widgets/FileField.tsx +0 -512
  128. package/src/widgets/FilterConditionField.tsx +0 -454
  129. package/src/widgets/FormulaField.tsx +0 -38
  130. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  131. package/src/widgets/GeolocationField.tsx +0 -193
  132. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  133. package/src/widgets/GridField.test.tsx +0 -632
  134. package/src/widgets/GridField.tsx +0 -1070
  135. package/src/widgets/ImageCropperDialog.tsx +0 -245
  136. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  137. package/src/widgets/ImageField.tsx +0 -305
  138. package/src/widgets/ImageLightbox.test.tsx +0 -52
  139. package/src/widgets/ImageLightbox.tsx +0 -108
  140. package/src/widgets/LocationField.tsx +0 -57
  141. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  142. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  143. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  144. package/src/widgets/LookupField.idField.test.tsx +0 -61
  145. package/src/widgets/LookupField.tsx +0 -1295
  146. package/src/widgets/MarkdownContent.tsx +0 -19
  147. package/src/widgets/MasterDetailField.tsx +0 -131
  148. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  149. package/src/widgets/MultiSelectField.tsx +0 -196
  150. package/src/widgets/NumberField.tsx +0 -49
  151. package/src/widgets/ObjectField.tsx +0 -94
  152. package/src/widgets/ObjectRefField.tsx +0 -121
  153. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  154. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  155. package/src/widgets/OptionsEmptyState.tsx +0 -99
  156. package/src/widgets/PasswordField.tsx +0 -49
  157. package/src/widgets/PeoplePicker.test.tsx +0 -360
  158. package/src/widgets/PeoplePicker.tsx +0 -579
  159. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  160. package/src/widgets/PercentField.tsx +0 -95
  161. package/src/widgets/PersonRow.tsx +0 -117
  162. package/src/widgets/PhoneField.tsx +0 -37
  163. package/src/widgets/QRCodeField.tsx +0 -103
  164. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  165. package/src/widgets/RadioField.tsx +0 -143
  166. package/src/widgets/RatingField.tsx +0 -94
  167. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  168. package/src/widgets/RecipientPickerField.tsx +0 -188
  169. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  170. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  171. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  172. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  173. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  174. package/src/widgets/RichTextField.tsx +0 -176
  175. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  176. package/src/widgets/SelectField.tsx +0 -193
  177. package/src/widgets/SelectionTray.tsx +0 -110
  178. package/src/widgets/SignatureField.tsx +0 -140
  179. package/src/widgets/SliderField.tsx +0 -39
  180. package/src/widgets/SummaryField.tsx +0 -31
  181. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  182. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  183. package/src/widgets/TagsField.tsx +0 -88
  184. package/src/widgets/TextAreaField.tsx +0 -213
  185. package/src/widgets/TextField.tsx +0 -45
  186. package/src/widgets/TimeField.tsx +0 -31
  187. package/src/widgets/UrlField.tsx +0 -46
  188. package/src/widgets/UserField.tsx +0 -59
  189. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  190. package/src/widgets/VectorField.tsx +0 -29
  191. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  192. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  193. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  194. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  195. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  196. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  197. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  198. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  199. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  200. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  201. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  202. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  203. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  204. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  205. package/src/widgets/address-format.ts +0 -120
  206. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  207. package/src/widgets/deriveLookupColumns.ts +0 -108
  208. package/src/widgets/file-size-guard.ts +0 -55
  209. package/src/widgets/file-value.test.ts +0 -230
  210. package/src/widgets/file-value.ts +0 -246
  211. package/src/widgets/nativeDateValue.test.ts +0 -115
  212. package/src/widgets/nativeDateValue.ts +0 -112
  213. package/src/widgets/openNativePicker.ts +0 -18
  214. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  215. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  216. package/src/widgets/personDisplay.test.ts +0 -113
  217. package/src/widgets/personDisplay.ts +0 -92
  218. package/src/widgets/recentLookups.test.ts +0 -45
  219. package/src/widgets/recentLookups.ts +0 -47
  220. package/src/widgets/toDomProps.ts +0 -204
  221. package/src/widgets/toHostGroupProps.ts +0 -102
  222. package/src/widgets/types.ts +0 -242
  223. package/src/widgets/useCascadingOptions.ts +0 -46
  224. package/src/widgets/useFieldTranslation.ts +0 -132
  225. package/src/widgets/useRecordQuery.test.ts +0 -174
  226. package/src/widgets/useRecordQuery.ts +0 -291
  227. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  228. package/src/widgets/useUploadingSignal.ts +0 -20
  229. 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
- }