@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,221 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * objectui#4361 — currency formatting overrode each currency's own ISO 4217
11
- * fraction-digit convention.
12
- *
13
- * `formatCurrency` picked its width from the VALUE's wholeness alone
14
- * (`isWhole ? 0 : 2`) and handed that literal `2` to `Intl`, which overrides
15
- * what `Intl` already knows about the currency being rendered. A yen amount
16
- * was printed with cents the currency does not have (`¥1,234.50`) and a dinar
17
- * amount with one digit fewer than it does have (`KWD 1.50`).
18
- *
19
- * The fix derives the currency's own digit count first
20
- * (`Intl.NumberFormat(undefined, { style: 'currency', currency })
21
- * .resolvedOptions().maximumFractionDigits`)
22
- * and switches wholeness against THAT instead of the literal 2. Two halves,
23
- * both pinned here:
24
- *
25
- * 1. **A fractional amount takes the currency's own width** — the defect.
26
- * JPY -> 0, USD/EUR/CNY -> 2, KWD/BHD -> 3.
27
- * 2. **A whole amount still drops the fraction entirely** — the Salesforce
28
- * convention `formatCurrency`'s doc comment states, that objectui#4033
29
- * pinned and objectui#4332 / PR #4362 re-pinned. It is not retired here,
30
- * it is EXTENDED consistently: `KWD 1` renders `KWD 1`, not the
31
- * `KWD 1.000` a bare `Intl` default would give. Dropping both bounds and
32
- * letting `Intl` decide would have fixed half 1 by breaking this half, so
33
- * the whole-amount cases below are controls, not decorations.
34
- *
35
- * The USD cases are the acceptance evidence for "byte-identical to today":
36
- * the #4033 and #4332/#4362 pins pass UNCHANGED, and the ones repeated here
37
- * are the same assertions run against the new derivation.
38
- *
39
- * Every expected string below was MEASURED against node 22 with full ICU
40
- * (icu 78.2) rather than reasoned about; where ICU's spacing or symbol choice
41
- * is what varies (and not the digit count this card is about), the assertion
42
- * is a regex on the digits.
43
- */
44
-
45
- import { describe, it, expect } from 'vitest';
46
- import React from 'react';
47
- import { render, screen } from '@testing-library/react';
48
- import '@testing-library/jest-dom';
49
- import { LocalizationProvider } from '@object-ui/i18n';
50
- import {
51
- formatCurrency,
52
- formatCompactCurrency,
53
- CurrencyCellRenderer,
54
- } from '../index';
55
- import { currencyFractionDigits } from '../currency';
56
-
57
- /**
58
- * ICU separates a currency CODE from the amount with U+00A0 (`KWD` + NBSP +
59
- * `1.500`), while a currency SYMBOL is written flush against it (`$1,234.50`).
60
- * Normalizing the NBSP keeps these pins about the DIGIT COUNT — which is what
61
- * this card is about — rather than about ICU's spacing choices, the same reason
62
- * objectui#4332's de-DE pin matched its separator loosely.
63
- *
64
- * Written as an escape rather than a pasted byte, per the repo's byte
65
- * discipline: a raw NBSP in source is invisible to every reader and unfindable
66
- * by grep in the spelling you would search for.
67
- */
68
- const normalizeNbsp = (s: string) => s.replace(/\u00a0/g, ' ');
69
-
70
- describe('the environment this card is measured in', () => {
71
- it('has full ICU — a small-ICU build would make every pin below meaningless', () => {
72
- // Written as an assertion rather than a comment: with a reduced ICU data
73
- // set the currency digit table is not present and JPY/KWD would resolve to
74
- // 2, so the fix would look correct while rendering the same wrong output.
75
- // If this one fails, nothing else in this file is evidence of anything.
76
- expect(currencyFractionDigits('JPY')).toBe(0);
77
- expect(currencyFractionDigits('KWD')).toBe(3);
78
- expect(currencyFractionDigits('USD')).toBe(2);
79
- });
80
- });
81
-
82
- describe('formatCurrency — a fractional amount takes the CURRENCY\'s width (objectui#4361)', () => {
83
- it('the card\'s two fixtures render as ISO 4217 says they should', () => {
84
- // Before: `¥1,234.50` — cents on a currency that has none.
85
- expect(formatCurrency(1234.5, 'JPY', 'en-US')).toBe('¥1,235');
86
- // Before: `KWD 1.50` — one digit short of the three a dinar has.
87
- expect(normalizeNbsp(formatCurrency(1.5, 'KWD', 'en-US'))).toBe('KWD 1.500');
88
- });
89
-
90
- it.each([
91
- // 0-digit currencies: the fraction is rounded away, not truncated.
92
- ['JPY', 1234.5, '¥1,235'],
93
- ['CLP', 1234.5, 'CLP 1,235'],
94
- ['ISK', 99.5, 'ISK 100'],
95
- // 3-digit currencies: the third digit is real and now survives.
96
- ['KWD', 1.5, 'KWD 1.500'],
97
- ['BHD', 2.5, 'BHD 2.500'],
98
- ['KWD', 1234.5678, 'KWD 1,234.568'],
99
- // 2-digit currencies: unchanged, by construction.
100
- ['USD', 1234.5, '$1,234.50'],
101
- ['EUR', 1234.5, '€1,234.50'],
102
- ])('%s %s renders %s', (currency, value, expected) => {
103
- expect(normalizeNbsp(formatCurrency(value as number, currency as string, 'en-US'))).toBe(
104
- expected as string,
105
- );
106
- });
107
-
108
- it('a 0-digit currency rounds the amount, it does not drop the digits', () => {
109
- // `1234.5` is not `1234`. Rounding is Intl's, and half-up here: the point
110
- // of the pin is that the value is not silently floored.
111
- expect(formatCurrency(1234.4, 'JPY', 'en-US')).toBe('¥1,234');
112
- expect(formatCurrency(1234.5, 'JPY', 'en-US')).toBe('¥1,235');
113
- });
114
- });
115
-
116
- describe('formatCurrency — CONTROL: the whole-number convention is extended, not retired', () => {
117
- it.each([
118
- // Salesforce convention, per currency: a whole amount shows no fraction.
119
- ['USD', 1234, '$1,234'],
120
- ['USD', 0, '$0'],
121
- ['EUR', 5000000, '€5,000,000'],
122
- // 3-digit currency: a bare `Intl` default would print `KWD 1.000` here.
123
- // The convention survives the fix and reaches currencies it never met.
124
- ['KWD', 1, 'KWD 1'],
125
- ['BHD', 250, 'BHD 250'],
126
- // 0-digit currency: both halves of the switch agree at 0, so a whole yen
127
- // amount is the one case that was already right and must stay right.
128
- ['JPY', 1234, '¥1,234'],
129
- ])('a whole %s amount drops the fraction: %s', (currency, value, expected) => {
130
- expect(normalizeNbsp(formatCurrency(value as number, currency as string, 'en-US'))).toBe(
131
- expected as string,
132
- );
133
- });
134
-
135
- it('a non-finite amount is not "whole" and still falls through unchanged', () => {
136
- expect(formatCurrency(Number.NaN, 'JPY', 'en-US')).toBe('¥NaN');
137
- });
138
- });
139
-
140
- describe('formatCurrency — CONTROL: the branches with no currency to derive from', () => {
141
- it('the no-currency branch keeps the literal 2 — there is nothing to derive', () => {
142
- // Deliberate: with no ISO code in hand there is no convention to follow,
143
- // so this branch keeps the historical width. Unchanged by this card.
144
- expect(formatCurrency(1234.5, undefined, 'en-US')).toBe('1,234.50');
145
- expect(formatCurrency(5000000, undefined, 'en-US')).toBe('5,000,000');
146
- });
147
-
148
- it('the bad-currency fallback is byte-identical to before the fix', () => {
149
- // The probe throws `RangeError: Invalid currency code` for a non-ISO code
150
- // exactly as `Intl` does, so it must NOT be allowed to escape and change
151
- // this fallback's width. It falls back to 2 and `formatDisplayNumber`'s own
152
- // throw then produces the same string objectui#4332 pinned.
153
- expect(formatCurrency(1234.5, 'NOT_A_CODE', 'en-US')).toBe('NOT_A_CODE 1234.50');
154
- });
155
-
156
- it('an unknown but well-formed code resolves to 2 without throwing', () => {
157
- // `ZZZ` is not in the CLDR digit table; ICU answers 2 for anything it does
158
- // not know, which is the right default and needs no special-casing here.
159
- expect(currencyFractionDigits('ZZZ')).toBe(2);
160
- expect(normalizeNbsp(formatCurrency(1.5, 'ZZZ', 'en-US'))).toBe('ZZZ 1.50');
161
- });
162
-
163
- it('formatCompactCurrency is a different function and is untouched', () => {
164
- // Compact notation has its own `maximumFractionDigits: 1` policy; this card
165
- // does not reach it. `¥1.2K` for JPY is compact's answer, not ISO 4217's.
166
- expect(formatCompactCurrency(1234.5, 'USD', 'en-US')).toBe('$1.2K');
167
- expect(formatCompactCurrency(1234.5, 'JPY', 'en-US')).toBe('¥1.2K');
168
- });
169
- });
170
-
171
- describe('formatCurrency — the display-locale path (a 0-digit and a 3-digit currency)', () => {
172
- it('a 0-digit currency in its own locale', () => {
173
- // ja-JP spells the symbol with the FULLWIDTH yen sign, en-US with the
174
- // ordinary one; either is ICU's business. The digit count is this card's.
175
- expect(formatCurrency(1234.5, 'JPY', 'ja-JP')).toMatch(/^[¥¥]1,235$/);
176
- });
177
-
178
- it('a 3-digit currency in a comma-decimal locale', () => {
179
- // de-DE: dot grouping, comma decimal mark, trailing code. Matched loosely
180
- // on the separator so an ICU spacing change cannot fail the digits.
181
- expect(formatCurrency(1.5, 'KWD', 'de-DE')).toMatch(/^1,500\s*KWD$/);
182
- expect(formatCurrency(1234.5, 'JPY', 'de-DE')).toMatch(/^1\.235\s*¥$/);
183
- });
184
-
185
- it('CONTROL: a 2-digit currency in the same locales is unchanged (objectui#4033)', () => {
186
- expect(formatCurrency(1234.5, 'EUR', 'de-DE')).toMatch(/^1\.234,50\s*€$/);
187
- expect(formatCurrency(1234.5, 'CNY', 'zh-CN')).toBe('¥1,234.50');
188
- });
189
- });
190
-
191
- describe('CurrencyCellRenderer — the user-visible surface (objectui#4361)', () => {
192
- const renderCurrency = (value: unknown, field: Record<string, unknown>, locale?: string, tenantCurrency?: string) =>
193
- render(
194
- <LocalizationProvider value={{ locale, currency: tenantCurrency }}>
195
- <CurrencyCellRenderer value={value as any} field={{ type: 'currency', ...field } as any} />
196
- </LocalizationProvider>,
197
- );
198
-
199
- it('a JPY grid cell shows no cents', () => {
200
- renderCurrency(1234.5, { currency: 'JPY' }, 'en-US');
201
- expect(screen.getByText('¥1,235')).toBeInTheDocument();
202
- });
203
-
204
- it('a KWD grid cell shows all three fils digits', () => {
205
- renderCurrency(1.5, { currency: 'KWD' }, 'en-US');
206
- expect(screen.getByText('KWD 1.500')).toBeInTheDocument();
207
- });
208
-
209
- it('the TENANT default currency reaches the same derivation (ADR-0053)', () => {
210
- // `resolveFieldCurrency` falls through field -> currencyConfig -> tenant
211
- // default, and the card's reachability rests on that third step: a JPY
212
- // tenant with a currency field that declares nothing.
213
- renderCurrency(1234.5, {}, 'en-US', 'JPY');
214
- expect(screen.getByText('¥1,235')).toBeInTheDocument();
215
- });
216
-
217
- it('CONTROL: a USD grid cell is byte-identical to objectui#4332', () => {
218
- renderCurrency(1234.5, { currency: 'USD' }, 'en-US');
219
- expect(screen.getByText('$1,234.50')).toBeInTheDocument();
220
- });
221
- });
@@ -1,148 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * The group-labelling DECLARATION, at the registration boundary (objectui#3961).
11
- *
12
- * A widget whose labelled surface is not a labelable HTML element cannot be
13
- * reached by a host's `<label for>` — the association has to go by IDREF
14
- * (`aria-labelledby`). Which widgets those are is a fact only the widget package
15
- * knows, so it is DECLARED (`ComponentMeta.labelling`) rather than guessed by the
16
- * form renderer from whatever DOM a widget happens to render.
17
- *
18
- * This file pins the declaration itself, separately from the rendered outcome
19
- * (`composite-group-label-e2e.test.tsx`), because the two can fail apart:
20
- *
21
- * • declared but not rendered → the widget's container never answers with a
22
- * role, so the name lands on a bare `div` (the e2e file catches that);
23
- * • rendered but not declared → the host keeps emitting `for`, the widget never
24
- * receives an `aria-labelledby`, and the group semantics are dead code. THIS
25
- * file is what fails then — an omission from the set is otherwise invisible,
26
- * since a missing declaration degrades silently to the single-control path.
27
- *
28
- * Nothing renders here, so `registerField`'s `React.lazy` wrapper is never
29
- * resolved: this reads metadata only.
30
- */
31
-
32
- import { describe, it, expect, beforeAll } from 'vitest';
33
- import { ComponentRegistry } from '@object-ui/core';
34
- import { registerAllFields, mapFieldTypeToFormType } from '../index';
35
-
36
- /**
37
- * Every field type whose host label must be associated by IDREF. Two shapes, one
38
- * declaration — see `FIELD_TYPES_GROUP_LABELLED` in `../index`:
39
- * real composites, plus `file`, whose single control is a `div[role="button"]`.
40
- *
41
- * `multiselect` (objectui#3975) is the seventh, added after #3961 shipped: its
42
- * chip row is the same wrapper `div` holding the host id that `checkboxes` had.
43
- * It is listed here rather than left to the e2e file alone because the omission
44
- * of a declaration is exactly the failure that degrades silently.
45
- */
46
- const GROUP_LABELLED = [
47
- 'address',
48
- 'geolocation',
49
- 'checkboxes',
50
- 'radio',
51
- 'rating',
52
- 'file',
53
- 'multiselect',
54
- ] as const;
55
-
56
- /**
57
- * Single-control widgets: the host's `<label for>` reaches a real labelable
58
- * element, so they must NOT be declared — that is the default path, and a stray
59
- * `labelling: 'group'` here would take a working `for` association away from a
60
- * control that needs it and hand it an `aria-labelledby` its markup ignores.
61
- */
62
- const CONTROL_LABELLED = [
63
- 'text',
64
- 'textarea',
65
- 'number',
66
- 'boolean',
67
- 'date',
68
- 'datetime',
69
- 'email',
70
- 'phone',
71
- 'url',
72
- 'currency',
73
- 'percent',
74
- 'password',
75
- 'tags',
76
- 'color',
77
- 'code',
78
- 'user',
79
- 'lookup',
80
- 'select',
81
- ] as const;
82
-
83
- beforeAll(() => {
84
- registerAllFields();
85
- });
86
-
87
- describe('field widgets declare how their label must be associated (objectui#3961)', () => {
88
- it.each(GROUP_LABELLED)('%s declares labelling: "group"', (type) => {
89
- expect(ComponentRegistry.getMeta(type, 'field')?.labelling).toBe('group');
90
- });
91
-
92
- it.each(CONTROL_LABELLED)('%s leaves labelling undeclared (⇒ "control")', (type) => {
93
- // Absent, not `'control'`: one spelling of the default, and the form
94
- // renderer reads any non-`'group'` value as the single-control path.
95
- expect(ComponentRegistry.getMeta(type, 'field')?.labelling).toBeUndefined();
96
- });
97
-
98
- it('declares group labelling for exactly the audited set — no more', () => {
99
- // A widget added to `fieldWidgetMap` later inherits the single-control path,
100
- // which is the safe default; if it is in fact a composite, the e2e pins
101
- // (objectui#3952's "every label resolves to a real control") go red rather
102
- // than this list quietly growing. This assertion is the other direction: it
103
- // fails if someone declares a widget `group` without an audited group
104
- // container to carry the name.
105
- const declared = ComponentRegistry.getKnownTypes()
106
- .filter((key) => key.startsWith('field:'))
107
- .filter((key) => ComponentRegistry.getMeta(key.slice('field:'.length), 'field')?.labelling === 'group')
108
- .map((key) => key.slice('field:'.length))
109
- .sort();
110
-
111
- expect(declared).toEqual([...GROUP_LABELLED].sort());
112
- });
113
- });
114
-
115
- /**
116
- * The join between the two halves (objectui#3986). The declaration above is
117
- * per-WIDGET; a form asks for a widget by the id its producer emitted. So the
118
- * declaration only reaches the right widget if the producer names the widget that
119
- * will actually render — and for `select` that depends on `multiple`, not on the
120
- * type string alone.
121
- *
122
- * Asserting the pair here, rather than in either half alone, is what makes the
123
- * failure visible: `mapFieldTypeToFormType`'s own test cannot know what
124
- * `labelling` the id it returns carries, and the declaration test cannot know
125
- * which id a `multiple: true` select resolves to. The gap between those two blind
126
- * spots is exactly where this bug lived.
127
- */
128
- describe('the widget id the object form emits carries the right declaration (objectui#3986)', () => {
129
- const labellingOf = (formType: string) =>
130
- ComponentRegistry.getMeta(formType.replace(/^field:/, ''), 'field')?.labelling;
131
-
132
- it('select + multiple resolves to a widget declared `group`', () => {
133
- // Before the producer fix this resolved to `field:select`, whose declaration
134
- // is (correctly) absent — so the host kept emitting a `for` at the chip row's
135
- // wrapper `div`, and the visible label named nothing.
136
- const formType = mapFieldTypeToFormType('select', { multiple: true });
137
- expect(formType).toBe('field:multiselect');
138
- expect(labellingOf(formType)).toBe('group');
139
- });
140
-
141
- it('a single-value select still resolves to an UNDECLARED widget', () => {
142
- // The guard direction. `select`'s trigger is a labelable
143
- // `button[role=combobox]`; declaring it `group` would strip a working `for`.
144
- const formType = mapFieldTypeToFormType('select');
145
- expect(formType).toBe('field:select');
146
- expect(labellingOf(formType)).toBeUndefined();
147
- });
148
- });
@@ -1,107 +0,0 @@
1
- /**
2
- * [framework#2926 ⑧] LookupCellRenderer must honor the target object's
3
- * configured display field. ObjectGrid forwards `display_field` on the
4
- * column meta (RELATIONAL_META_KEYS) exactly like `reference`, but the read
5
- * cell used to ignore it and always ran the hardcoded heuristics — `name`
6
- * first — so a target object whose displayNameField is a localized/label
7
- * field still rendered the raw API `name`.
8
- */
9
- import { describe, it, expect, vi } from 'vitest';
10
- import { render, screen, waitFor } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import React from 'react';
13
-
14
- import { LookupCellRenderer } from '../index';
15
- import { SchemaRendererProvider } from '@object-ui/react';
16
-
17
- const ID_A = '01ARZ3NDEKTSV4RRFFQ69G5FAA';
18
- const ID_B = '01ARZ3NDEKTSV4RRFFQ69G5FAB';
19
-
20
- function makeDataSource() {
21
- const findOne = vi.fn(async (object: string, id: string) => {
22
- if (object === 'showcase_category') {
23
- // Record carries BOTH the raw API name and a localized label field.
24
- if (id === ID_A) return { id, name: 'cat_hardware', label_zh: '硬件' };
25
- if (id === ID_B) return { id, name: 'cat_software', label_zh: '软件' };
26
- }
27
- return null;
28
- });
29
- return { findOne, find: vi.fn() } as any;
30
- }
31
-
32
- describe('LookupCellRenderer — display_field resolution', () => {
33
- it('prefers the configured display_field over the heuristic `name`', async () => {
34
- const ds = makeDataSource();
35
- render(
36
- <SchemaRendererProvider dataSource={ds}>
37
- <LookupCellRenderer
38
- value={ID_A}
39
- field={{ type: 'lookup', reference: 'showcase_category', display_field: 'label_zh' } as any}
40
- />
41
- </SchemaRendererProvider>,
42
- );
43
- await waitFor(() => {
44
- expect(screen.getByText('硬件')).toBeInTheDocument();
45
- });
46
- expect(screen.queryByText('cat_hardware')).not.toBeInTheDocument();
47
- });
48
-
49
- it('keeps the heuristic (`name` first) when no display_field is configured', async () => {
50
- const ds = makeDataSource();
51
- render(
52
- <SchemaRendererProvider dataSource={ds}>
53
- <LookupCellRenderer
54
- value={ID_A}
55
- field={{ type: 'lookup', reference: 'showcase_category' } as any}
56
- />
57
- </SchemaRendererProvider>,
58
- );
59
- await waitFor(() => {
60
- expect(screen.getByText('cat_hardware')).toBeInTheDocument();
61
- });
62
- });
63
-
64
- it('uses display_field on server-expanded nested objects (no fetch path)', () => {
65
- const ds = makeDataSource();
66
- render(
67
- <SchemaRendererProvider dataSource={ds}>
68
- <LookupCellRenderer
69
- value={{ id: ID_A, name: 'cat_hardware', label_zh: '硬件' } as any}
70
- field={{ type: 'lookup', reference: 'showcase_category', display_field: 'label_zh' } as any}
71
- />
72
- </SchemaRendererProvider>,
73
- );
74
- expect(screen.getByText('硬件')).toBeInTheDocument();
75
- expect(ds.findOne).not.toHaveBeenCalled();
76
- });
77
-
78
- it('does not serve a cached heuristic name to a display_field column (cache key isolation)', async () => {
79
- const ds = makeDataSource();
80
- // First: a column WITHOUT display_field resolves and caches the heuristic name.
81
- const first = render(
82
- <SchemaRendererProvider dataSource={ds}>
83
- <LookupCellRenderer
84
- value={ID_B}
85
- field={{ type: 'lookup', reference: 'showcase_category' } as any}
86
- />
87
- </SchemaRendererProvider>,
88
- );
89
- await waitFor(() => {
90
- expect(screen.getByText('cat_software')).toBeInTheDocument();
91
- });
92
- first.unmount();
93
- // Then: a column WITH display_field for the same record must show the
94
- // configured field, not the previously cached heuristic name.
95
- render(
96
- <SchemaRendererProvider dataSource={ds}>
97
- <LookupCellRenderer
98
- value={ID_B}
99
- field={{ type: 'lookup', reference: 'showcase_category', display_field: 'label_zh' } as any}
100
- />
101
- </SchemaRendererProvider>,
102
- );
103
- await waitFor(() => {
104
- expect(screen.getByText('软件')).toBeInTheDocument();
105
- });
106
- });
107
- });
@@ -1,119 +0,0 @@
1
- /**
2
- * Regression for objectstack-ai/objectui#2357: the lookup chip
3
- * (LookupCellRenderer — record header highlight + detail sections) resolved
4
- * display names via the local key heuristic (`pickRecordDisplayName`), whose
5
- * `_number`/`_code` suffix scan surfaced an autonumber (`0001`) over the
6
- * record's real name whenever the referenced object declared its title in
7
- * `nameField`/`titleFormat`. The picker (LookupField) already resolved via the
8
- * unified ADR-0079 resolver, so the two surfaces disagreed.
9
- *
10
- * The chip must now resolve through the referenced object's schema
11
- * (`dataSource.getObjectSchema`) with the precedence
12
- * `displayField → nameField/titleFormat → type-aware derivation`, keeping the
13
- * key heuristic only as the no-schema fallback.
14
- */
15
- import { describe, it, expect, vi } from 'vitest';
16
- import { render, screen, waitFor } from '@testing-library/react';
17
- import '@testing-library/jest-dom';
18
- import React from 'react';
19
-
20
- import { LookupCellRenderer } from '../index';
21
- import { SchemaRendererProvider } from '@object-ui/react';
22
-
23
- // The expanded record: the autonumber (`req_number`) precedes the real name
24
- // (`req_name`) in key order, so the OLD heuristic-only path rendered "0001".
25
- const EXPANDED = { id: 'rec1', req_number: '0001', req_name: 'Annual Maintenance' };
26
-
27
- function makeDataSource(objectName: string, schema: any, record?: any) {
28
- return {
29
- find: vi.fn(),
30
- findOne: vi.fn(async (object: string, id: string) =>
31
- object === objectName && record && id === record.id ? record : null,
32
- ),
33
- getObjectSchema: vi.fn(async (object: string) =>
34
- object === objectName ? schema : null,
35
- ),
36
- } as any;
37
- }
38
-
39
- describe('LookupCellRenderer — schema-aware display name (issue #2357)', () => {
40
- it('expanded object: nameField beats the autonumber suffix heuristic', async () => {
41
- const ds = makeDataSource('mtc_request_a', { nameField: 'req_name' });
42
- render(
43
- <SchemaRendererProvider dataSource={ds}>
44
- <LookupCellRenderer
45
- value={EXPANDED}
46
- field={{ type: 'lookup', reference_to: 'mtc_request_a' } as any}
47
- />
48
- </SchemaRendererProvider>,
49
- );
50
- await waitFor(() => {
51
- expect(screen.getByText('Annual Maintenance')).toBeInTheDocument();
52
- });
53
- expect(screen.queryByText('0001')).not.toBeInTheDocument();
54
- });
55
-
56
- it('expanded object: titleFormat template resolves when no nameField', async () => {
57
- const ds = makeDataSource('mtc_request_b', { titleFormat: '{req_name} - {req_number}' });
58
- render(
59
- <SchemaRendererProvider dataSource={ds}>
60
- <LookupCellRenderer
61
- value={{ ...EXPANDED }}
62
- field={{ type: 'lookup', reference_to: 'mtc_request_b' } as any}
63
- />
64
- </SchemaRendererProvider>,
65
- );
66
- await waitFor(() => {
67
- expect(screen.getByText('Annual Maintenance - 0001')).toBeInTheDocument();
68
- });
69
- });
70
-
71
- it('explicit displayField wins over the schema nameField', async () => {
72
- const ds = makeDataSource('mtc_request_c', { nameField: 'req_name' });
73
- render(
74
- <SchemaRendererProvider dataSource={ds}>
75
- <LookupCellRenderer
76
- value={{ ...EXPANDED }}
77
- field={{ type: 'lookup', reference_to: 'mtc_request_c', display_field: 'req_number' } as any}
78
- />
79
- </SchemaRendererProvider>,
80
- );
81
- // Author explicitly asked for the number — honour it (also the documented
82
- // workaround in the issue).
83
- expect(await screen.findByText('0001')).toBeInTheDocument();
84
- });
85
-
86
- it('fetch-on-demand primitive id resolves via the schema nameField too', async () => {
87
- // Opaque ULID-ish id so the old path would have resolved the fetched
88
- // record through the heuristic and shown its autonumber.
89
- const id = '01ARZ3NDEKTSV4RRFFQ69G5FBB';
90
- const record = { id, req_number: '0007', req_name: 'Pump Overhaul' };
91
- const ds = makeDataSource('mtc_request_d', { nameField: 'req_name' }, record);
92
- render(
93
- <SchemaRendererProvider dataSource={ds}>
94
- <LookupCellRenderer
95
- value={id}
96
- field={{ type: 'lookup', reference_to: 'mtc_request_d' } as any}
97
- />
98
- </SchemaRendererProvider>,
99
- );
100
- await waitFor(() => {
101
- expect(screen.getByText('Pump Overhaul')).toBeInTheDocument();
102
- });
103
- expect(screen.queryByText('0007')).not.toBeInTheDocument();
104
- expect(ds.getObjectSchema).toHaveBeenCalledWith('mtc_request_d');
105
- });
106
-
107
- it('falls back to the key heuristic when the data source has no schema API', () => {
108
- const ds = { find: vi.fn(), findOne: vi.fn() } as any; // no getObjectSchema
109
- render(
110
- <SchemaRendererProvider dataSource={ds}>
111
- <LookupCellRenderer
112
- value={{ id: 'rec2', framework_name: 'SOX' }}
113
- field={{ type: 'lookup', reference_to: 'mtc_request_e' } as any}
114
- />
115
- </SchemaRendererProvider>,
116
- );
117
- expect(screen.getByText('SOX')).toBeInTheDocument();
118
- });
119
- });