@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,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,33 +0,0 @@
1
- import React from 'react';
2
- import { EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
-
5
- /**
6
- * FormulaField - Read-only computed field
7
- * Values are computed on the backend and cannot be edited
8
- */
9
- export function FormulaField({ value, field, ...props }: FieldWidgetComponentProps<any>) {
10
- const formulaField = field as any;
11
- const returnType = formulaField?.return_type || 'text';
12
-
13
- if (value == null) {
14
- return <EmptyValue className={props.className} />;
15
- }
16
-
17
- let displayValue: string;
18
- if (returnType === 'number' || returnType === 'currency') {
19
- displayValue = typeof value === 'number' ? value.toFixed(2) : String(value);
20
- } else if (returnType === 'boolean') {
21
- displayValue = value ? 'Yes' : 'No';
22
- } else if (returnType === 'date') {
23
- displayValue = new Date(value).toLocaleDateString();
24
- } else {
25
- displayValue = String(value);
26
- }
27
-
28
- return (
29
- <span className={`text-sm font-mono text-gray-700 ${props.className || ''}`}>
30
- {displayValue}
31
- </span>
32
- );
33
- }