@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.
- package/CHANGELOG.md +6150 -0
- package/README.md +108 -8
- package/dist/FieldEditWidget.d.ts +70 -8
- package/dist/coerceToSafeValue.d.ts +54 -0
- package/dist/currency.d.ts +1 -1
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +5 -5
- package/dist/index.css +53 -11
- package/dist/index.d.ts +653 -128
- package/dist/index.js +4275 -3171
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +20 -4
- package/dist/widgets/BooleanField.d.ts +2 -2
- package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/DateField.d.ts +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +43 -2
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +10 -4
- package/dist/widgets/GridField.d.ts +102 -86
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +31 -4
- package/dist/widgets/LookupField.d.ts +9 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +10 -3
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +38 -8
- package/dist/widgets/PasswordField.d.ts +15 -1
- package/dist/widgets/PeoplePicker.d.ts +17 -2
- package/dist/widgets/PercentField.d.ts +3 -2
- package/dist/widgets/PersonRow.d.ts +13 -2
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +44 -1
- package/dist/widgets/RecordPickerDialog.d.ts +33 -16
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SelectionTray.d.ts +13 -2
- package/dist/widgets/SignatureField.d.ts +32 -2
- package/dist/widgets/SliderField.d.ts +37 -2
- package/dist/widgets/SummaryField.d.ts +1 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +6 -2
- package/dist/widgets/TextField.d.ts +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/UserField.d.ts +1 -1
- package/dist/widgets/VectorField.d.ts +1 -1
- package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +27 -7
- package/dist/widgets/file-affordance.d.ts +33 -0
- package/dist/widgets/file-size-guard.d.ts +5 -23
- package/dist/widgets/file-value.d.ts +38 -9
- package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
- package/dist/widgets/nativeDateValue.d.ts +9 -27
- package/dist/widgets/number-format.d.ts +137 -0
- package/dist/widgets/numberBadInput.d.ts +135 -0
- package/dist/widgets/personDisplay.d.ts +22 -4
- package/dist/widgets/richTextDisplay.d.ts +116 -0
- package/dist/widgets/toDomProps.d.ts +10 -1
- package/dist/widgets/toHostGroupProps.d.ts +172 -21
- package/dist/widgets/toHostProps.d.ts +105 -0
- package/dist/widgets/types.d.ts +19 -6
- package/dist/widgets/upload-error-message.d.ts +6 -0
- package/dist/widgets/uploadingScope.d.ts +142 -0
- package/dist/widgets/useCascadingOptions.d.ts +3 -2
- package/dist/widgets/useRecordQuery.d.ts +4 -2
- package/dist/widgets/useUploadingSignal.d.ts +8 -0
- package/package.json +11 -13
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -207
- package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
- package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
- package/src/__tests__/DateCellRenderer.test.tsx +0 -82
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
- package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
- package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
- package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
- package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
- package/src/__tests__/date-locale-channel.test.tsx +0 -305
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
- package/src/__tests__/group-labelling-declaration.test.ts +0 -148
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
- package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
- package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -924
- package/src/currency.ts +0 -144
- package/src/datetime-widgets.test.tsx +0 -185
- package/src/field-type-alias.multiple.test.ts +0 -81
- package/src/field-type-alias.ts +0 -156
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.css +0 -44
- package/src/index.tsx +0 -2745
- package/src/standard-widgets.test.tsx +0 -364
- package/src/widgets/AddressField.postalCode.test.tsx +0 -257
- package/src/widgets/AddressField.tsx +0 -182
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -86
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -169
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -135
- package/src/widgets/DateField.tsx +0 -40
- package/src/widgets/DateTimeField.tsx +0 -46
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -512
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -38
- package/src/widgets/FullscreenFieldEditor.tsx +0 -60
- package/src/widgets/GeolocationField.tsx +0 -193
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -632
- package/src/widgets/GridField.tsx +0 -1070
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.maxSize.test.tsx +0 -154
- package/src/widgets/ImageField.tsx +0 -305
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1295
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -196
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -99
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -143
- package/src/widgets/RatingField.tsx +0 -94
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1365
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -193
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -59
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/address-format.ts +0 -120
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-size-guard.ts +0 -55
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/nativeDateValue.test.ts +0 -115
- package/src/widgets/nativeDateValue.ts +0 -112
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -204
- package/src/widgets/toHostGroupProps.ts +0 -102
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -132
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -291
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
package/src/currency.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the MIT license found in the LICENSE file.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* `resolveFieldCurrency` now lives in `@object-ui/i18n` (co-located with
|
|
10
|
-
* `useLocalization`, which supplies the tenant default). This module re-exports
|
|
11
|
-
* it so the long-standing `@object-ui/fields` import path keeps working for
|
|
12
|
-
* every field/cell renderer that already depends on it.
|
|
13
|
-
*/
|
|
14
|
-
export { resolveFieldCurrency } from '@object-ui/i18n';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Fallback width for a currency whose digit count cannot be resolved: the
|
|
18
|
-
* literal both call sites used unconditionally before objectui#4361. It is
|
|
19
|
-
* reached only when there is no ISO code to ask about (an invalid code, or a
|
|
20
|
-
* runtime with no currency data at all), never as a policy.
|
|
21
|
-
*/
|
|
22
|
-
const DEFAULT_FRACTION_DIGITS = 2;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* ISO 4217 minor-unit width, memoized per currency code.
|
|
26
|
-
*
|
|
27
|
-
* The probe is deliberately made with NO locale. Measured across
|
|
28
|
-
* en-US / de-DE / ja-JP / ar-KW / zh-CN / fr-FR / pl-PL / es-ES and the runtime
|
|
29
|
-
* default, every locale answers the same digit count for the same code (USD 2,
|
|
30
|
-
* JPY 0, KWD 3, BHD 3, CLP 0, ISK 0) — the count comes from CLDR's
|
|
31
|
-
* `currencyData`, which is keyed by the currency, not by who is reading it.
|
|
32
|
-
* Dropping the locale from the probe therefore loses nothing and removes a
|
|
33
|
-
* failure mode: a malformed locale tag makes `Intl.NumberFormat` throw
|
|
34
|
-
* `RangeError: Incorrect locale information provided`, which would turn a bad
|
|
35
|
-
* LOCALE into a wrong CURRENCY width. It also makes the code the whole cache
|
|
36
|
-
* key.
|
|
37
|
-
*/
|
|
38
|
-
const fractionDigitsByCurrency = new Map<string, number>();
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The number of fraction digits ISO 4217 gives `currency` — 0 for JPY/KRW/CLP,
|
|
42
|
-
* 2 for USD/EUR/CNY, 3 for KWD/BHD/OMR/TND — as ICU reports it.
|
|
43
|
-
*
|
|
44
|
-
* Returns {@link DEFAULT_FRACTION_DIGITS} when the code is not a currency code
|
|
45
|
-
* at all: `Intl` throws `RangeError: Invalid currency code` for those, and the
|
|
46
|
-
* throw must not escape, or a bad code would change the width used by the
|
|
47
|
-
* callers' own bad-currency fallbacks (objectui#4332 pinned those strings). A
|
|
48
|
-
* code that is well-formed but unknown to CLDR (e.g. `ZZZ`) does not throw —
|
|
49
|
-
* ICU answers 2 for anything outside its table, which is the same default.
|
|
50
|
-
*
|
|
51
|
-
* Memoized because the currency cell renderer runs once per grid cell: measured
|
|
52
|
-
* at 24.3us per uncached probe against 0.02us cached (200k iterations, node 22),
|
|
53
|
-
* so a 500-row grid pays ~12ms per render pass for a value that cannot change.
|
|
54
|
-
*/
|
|
55
|
-
export function currencyFractionDigits(currency: string): number {
|
|
56
|
-
const cached = fractionDigitsByCurrency.get(currency);
|
|
57
|
-
if (cached !== undefined) return cached;
|
|
58
|
-
let digits = DEFAULT_FRACTION_DIGITS;
|
|
59
|
-
try {
|
|
60
|
-
// `maximumFractionDigits` is typed `number | undefined` on
|
|
61
|
-
// `ResolvedNumberFormatOptions` — it is absent when a format resolves with
|
|
62
|
-
// significant-digit rounding instead of fraction-digit rounding, which a
|
|
63
|
-
// bare `style: 'currency'` never does. Coalesced rather than asserted so
|
|
64
|
-
// the value is a number by construction and not by claim.
|
|
65
|
-
digits =
|
|
66
|
-
new Intl.NumberFormat(undefined, { style: 'currency', currency }).resolvedOptions()
|
|
67
|
-
.maximumFractionDigits ?? DEFAULT_FRACTION_DIGITS;
|
|
68
|
-
} catch {
|
|
69
|
-
// Not a currency code — keep the historical width and let the caller's own
|
|
70
|
-
// formatting attempt surface the bad code.
|
|
71
|
-
}
|
|
72
|
-
fractionDigitsByCurrency.set(currency, digits);
|
|
73
|
-
return digits;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Display symbols, memoized per **(locale, currency)** pair.
|
|
78
|
-
*
|
|
79
|
-
* The pair, not the code — and this is the one place this helper deliberately
|
|
80
|
-
* parts company with {@link currencyFractionDigits} above. The digit count is
|
|
81
|
-
* locale-independent (measured across eight locales, see that map's note), so
|
|
82
|
-
* its probe drops the locale and keys the cache on the code alone. The SYMBOL
|
|
83
|
-
* is genuinely locale-dependent, measured on the same runtime:
|
|
84
|
-
*
|
|
85
|
-
* - `USD` → `$` at `en` / `en-US` / `de-DE` / `ja-JP`, `US$` at `en-GB` and
|
|
86
|
-
* `zh-CN`, `$US` at `fr-FR`;
|
|
87
|
-
* - `JPY` → U+00A5 at `en`, the FULLWIDTH U+FFE5 at `ja-JP`, `JP¥` at
|
|
88
|
-
* `en-GB`, and the bare code `JPY` at `fr-FR`.
|
|
89
|
-
*
|
|
90
|
-
* Copying the code-only cache key would therefore let whichever locale asked
|
|
91
|
-
* first answer for every locale after it, for the life of the process — a
|
|
92
|
-
* silent wrong answer rather than a crash, so it is pinned by test.
|
|
93
|
-
*
|
|
94
|
-
* `|` is safe as the key separator: it appears in neither a BCP-47 tag nor an
|
|
95
|
-
* ISO 4217 code, so no two distinct pairs can collide on one key.
|
|
96
|
-
*/
|
|
97
|
-
const symbolByLocaleAndCurrency = new Map<string, string>();
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* The string ICU renders in place of `currency` for a reader in `locale` — `$`
|
|
101
|
-
* for USD at `en`, `€` for EUR, U+00A5 for JPY — taken from the `currency` part
|
|
102
|
-
* of the very format the display path already uses.
|
|
103
|
-
*
|
|
104
|
-
* This is the ONE channel for the symbol (objectui#4414). It is not an
|
|
105
|
-
* independent opinion about how a currency should look: it is the same
|
|
106
|
-
* `Intl.NumberFormat(locale, { style: 'currency', currency })` that
|
|
107
|
-
* `formatDisplayNumber` formats amounts with, read one part at a time. So a
|
|
108
|
-
* widget that renders an amount in one mode and a bare symbol in another cannot
|
|
109
|
-
* disagree with itself — which is exactly what the hand-written
|
|
110
|
-
* `currency === 'USD' ? '$' : currency` ternary did, showing `JPY` beside a
|
|
111
|
-
* readonly `¥1,235`.
|
|
112
|
-
*
|
|
113
|
-
* Falls back to `currency` itself — the code as given — in the three cases
|
|
114
|
-
* where ICU cannot answer, because a bare code is a legitimate adornment (it is
|
|
115
|
-
* what ICU itself returns for KWD/BHD/CHF/ISK, and what
|
|
116
|
-
* `formatAmount`'s own bad-code branch renders beside it):
|
|
117
|
-
*
|
|
118
|
-
* 1. an invalid currency code — `Intl` throws `RangeError: Invalid currency
|
|
119
|
-
* code`;
|
|
120
|
-
* 2. a malformed locale tag (`en_US` with an underscore, say) — `Intl` throws
|
|
121
|
-
* `RangeError: Incorrect locale information provided`. `currencyFractionDigits`
|
|
122
|
-
* dodged this failure mode by dropping the locale from its probe; this
|
|
123
|
-
* helper needs the locale, so it catches instead. Neither throw may escape:
|
|
124
|
-
* this runs during render, so an escaped `RangeError` is a blank widget.
|
|
125
|
-
* 3. no `currency` part in the output at all — not observed for
|
|
126
|
-
* `style: 'currency'`, but coalesced rather than asserted.
|
|
127
|
-
*/
|
|
128
|
-
export function currencySymbol(currency: string, locale: string): string {
|
|
129
|
-
const key = `${locale}|${currency}`;
|
|
130
|
-
const cached = symbolByLocaleAndCurrency.get(key);
|
|
131
|
-
if (cached !== undefined) return cached;
|
|
132
|
-
let symbol = currency;
|
|
133
|
-
try {
|
|
134
|
-
// The formatted value is irrelevant — only the `currency` part is read — so
|
|
135
|
-
// the cheapest deterministic input is used.
|
|
136
|
-
const parts = new Intl.NumberFormat(locale, { style: 'currency', currency }).formatToParts(0);
|
|
137
|
-
symbol = parts.find((part) => part.type === 'currency')?.value ?? currency;
|
|
138
|
-
} catch {
|
|
139
|
-
// Bad code or bad locale — keep the code, and let the caller's own
|
|
140
|
-
// formatting attempt surface the real problem.
|
|
141
|
-
}
|
|
142
|
-
symbolByLocaleAndCurrency.set(key, symbol);
|
|
143
|
-
return symbol;
|
|
144
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
-
import { DateField } from './widgets/DateField';
|
|
5
|
-
import { DateTimeField } from './widgets/DateTimeField';
|
|
6
|
-
import { TimeField } from './widgets/TimeField';
|
|
7
|
-
import type { FieldWidgetComponentProps } from './widgets/types';
|
|
8
|
-
|
|
9
|
-
const mockField = {
|
|
10
|
-
name: 'test_field',
|
|
11
|
-
label: 'Test Field',
|
|
12
|
-
type: 'date',
|
|
13
|
-
} as any;
|
|
14
|
-
|
|
15
|
-
const baseProps: FieldWidgetComponentProps<string> = {
|
|
16
|
-
field: mockField,
|
|
17
|
-
value: '',
|
|
18
|
-
onChange: vi.fn(),
|
|
19
|
-
readonly: false,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
describe('Date/Time Widgets', () => {
|
|
23
|
-
describe('DateField', () => {
|
|
24
|
-
it('renders date input in edit mode', () => {
|
|
25
|
-
const { container } = render(<DateField {...baseProps} />);
|
|
26
|
-
const dateInput = container.querySelector('input[type="date"]');
|
|
27
|
-
expect(dateInput).toBeInTheDocument();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('calls onChange when value changes', () => {
|
|
31
|
-
const { container } = render(<DateField {...baseProps} />);
|
|
32
|
-
const input = container.querySelector('input[type="date"]') as HTMLInputElement;
|
|
33
|
-
fireEvent.change(input, { target: { value: '2023-01-01' } });
|
|
34
|
-
expect(baseProps.onChange).toHaveBeenCalledWith('2023-01-01');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('renders formatted date in readonly mode', () => {
|
|
38
|
-
render(<DateField {...baseProps} readonly value="2023-01-01" />);
|
|
39
|
-
const date = new Date('2023-01-01');
|
|
40
|
-
expect(screen.getByText(date.toLocaleDateString())).toBeInTheDocument();
|
|
41
|
-
expect(document.querySelector('input')).not.toBeInTheDocument();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('renders dash for empty readonly value', () => {
|
|
45
|
-
render(<DateField {...baseProps} readonly value={''} />);
|
|
46
|
-
expect(screen.getByText('—')).toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('DateTimeField', () => {
|
|
51
|
-
it('renders datetime-local input in edit mode', () => {
|
|
52
|
-
const { container } = render(<DateTimeField {...baseProps} />);
|
|
53
|
-
const input = container.querySelector('input[type="datetime-local"]');
|
|
54
|
-
expect(input).toBeInTheDocument();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('calls onChange when value changes', () => {
|
|
58
|
-
const onChange = vi.fn();
|
|
59
|
-
const { container } = render(<DateTimeField {...baseProps} onChange={onChange} />);
|
|
60
|
-
const input = container.querySelector('input[type="datetime-local"]') as HTMLInputElement;
|
|
61
|
-
fireEvent.change(input, { target: { value: '2023-01-01T12:00' } });
|
|
62
|
-
// The control speaks zone-less local wall clock; form state holds
|
|
63
|
-
// ISO, the shape the API hands back and `toDateTimeInputValue`
|
|
64
|
-
// reads (objectui#3127). Storing the naive string instead would put
|
|
65
|
-
// read and write on different timezone bases.
|
|
66
|
-
expect(onChange).toHaveBeenCalledWith(new Date('2023-01-01T12:00').toISOString());
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('renders formatted datetime in readonly mode', () => {
|
|
70
|
-
const val = '2023-01-01T12:00:00.000Z';
|
|
71
|
-
render(<DateTimeField {...baseProps} readonly value={val} />);
|
|
72
|
-
const date = new Date(val);
|
|
73
|
-
const span = screen.getByText((content) => {
|
|
74
|
-
return content.includes(date.toLocaleDateString());
|
|
75
|
-
});
|
|
76
|
-
expect(span).toBeInTheDocument();
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* objectui#3127 — the record's stored ISO value reached the native control
|
|
82
|
-
* verbatim, and `<input type="date">` / `<input type="datetime-local">`
|
|
83
|
-
* reject anything but their one exact shape SILENTLY: the attribute lands
|
|
84
|
-
* in the DOM, `.value` reads back `''`, and the user sees an empty box on a
|
|
85
|
-
* field that has a value. `.value` (not `getAttribute('value')`) is
|
|
86
|
-
* therefore the only assertion that can see this bug at all.
|
|
87
|
-
*/
|
|
88
|
-
describe('existing values round-trip through the native controls (#3127)', () => {
|
|
89
|
-
// The seeded showcase value. Whole minutes and no sub-second part, so
|
|
90
|
-
// the control (which has minute resolution) can represent it exactly
|
|
91
|
-
// and the round trip below is lossless in every timezone.
|
|
92
|
-
const storedIso = '2026-06-17T14:30:00.000Z';
|
|
93
|
-
|
|
94
|
-
it('DateTimeField shows a stored ISO instant instead of an empty box', () => {
|
|
95
|
-
const { container } = render(<DateTimeField {...baseProps} value={storedIso} />);
|
|
96
|
-
const input = container.querySelector('input[type="datetime-local"]') as HTMLInputElement;
|
|
97
|
-
|
|
98
|
-
expect(input.value).not.toBe('');
|
|
99
|
-
expect(input.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
|
|
100
|
-
// The shown wall clock denotes the SAME instant as the record —
|
|
101
|
-
// asserted this way so the test holds in every timezone, which a
|
|
102
|
-
// hardcoded "2026-06-17T22:30" would not.
|
|
103
|
-
expect(new Date(input.value).getTime()).toBe(new Date(storedIso).getTime());
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('DateTimeField writes back on the same basis it reads', () => {
|
|
107
|
-
// Read and write must share one timezone basis. Displaying local
|
|
108
|
-
// while storing the naive local string is the worse bug: the value
|
|
109
|
-
// would silently drift by the viewer's offset on every edit.
|
|
110
|
-
const onChange = vi.fn();
|
|
111
|
-
const { container } = render(
|
|
112
|
-
<DateTimeField {...baseProps} onChange={onChange} value={storedIso} />
|
|
113
|
-
);
|
|
114
|
-
const input = container.querySelector('input[type="datetime-local"]') as HTMLInputElement;
|
|
115
|
-
|
|
116
|
-
fireEvent.change(input, { target: { value: '2026-06-18T09:15' } });
|
|
117
|
-
|
|
118
|
-
const written = onChange.mock.calls[0][0];
|
|
119
|
-
// Zone-QUALIFIED, which the control's own output is not. Storing
|
|
120
|
-
// the raw `2026-06-18T09:15` is what puts the write on a different
|
|
121
|
-
// basis from the read: the server has no way to know whose 09:15
|
|
122
|
-
// it is, and resolves it to a different instant than the one the
|
|
123
|
-
// user picked.
|
|
124
|
-
expect(written).toMatch(/(Z|[+-]\d{2}:\d{2})$/);
|
|
125
|
-
expect(new Date(written).getTime()).toBe(new Date('2026-06-18T09:15').getTime());
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('DateTimeField returning to the shown minute restores the exact stored value', () => {
|
|
129
|
-
// Guards the drift this fix could otherwise introduce: if the two
|
|
130
|
-
// conversions disagreed by the viewer's UTC offset, navigating away
|
|
131
|
-
// from a value and back would land somewhere else, and every open →
|
|
132
|
-
// fiddle → undo would quietly move the record.
|
|
133
|
-
// Held in real state, because a `vi.fn()` onChange would leave the
|
|
134
|
-
// control pinned to its initial prop and React would swallow the
|
|
135
|
-
// second edit as a no-op — the test would pass without exercising
|
|
136
|
-
// anything.
|
|
137
|
-
const seen: string[] = [];
|
|
138
|
-
const Host = () => {
|
|
139
|
-
const [v, setV] = React.useState<string>(storedIso);
|
|
140
|
-
seen.push(v);
|
|
141
|
-
return <DateTimeField {...baseProps} value={v} onChange={setV as any} />;
|
|
142
|
-
};
|
|
143
|
-
const { container } = render(<Host />);
|
|
144
|
-
const input = container.querySelector('input[type="datetime-local"]') as HTMLInputElement;
|
|
145
|
-
const shown = input.value;
|
|
146
|
-
|
|
147
|
-
fireEvent.change(input, { target: { value: '2026-06-18T09:15' } });
|
|
148
|
-
fireEvent.change(input, { target: { value: shown } });
|
|
149
|
-
|
|
150
|
-
expect(seen[seen.length - 1]).toBe(storedIso);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('DateField shows the stored calendar day, ISO-shaped or not', () => {
|
|
154
|
-
// Both spellings the API may hand back for a `date` field. The day
|
|
155
|
-
// must survive verbatim — re-parsing the bare form as UTC midnight
|
|
156
|
-
// would move it to the 16th west of Greenwich.
|
|
157
|
-
for (const stored of ['2026-06-17', '2026-06-17T00:00:00.000Z']) {
|
|
158
|
-
const { container, unmount } = render(<DateField {...baseProps} value={stored} />);
|
|
159
|
-
const input = container.querySelector('input[type="date"]') as HTMLInputElement;
|
|
160
|
-
expect(input.value).toBe('2026-06-17');
|
|
161
|
-
unmount();
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('TimeField', () => {
|
|
167
|
-
it('renders time input in edit mode', () => {
|
|
168
|
-
const { container } = render(<TimeField {...baseProps} />);
|
|
169
|
-
const input = container.querySelector('input[type="time"]');
|
|
170
|
-
expect(input).toBeInTheDocument();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('calls onChange when value changes', () => {
|
|
174
|
-
const { container } = render(<TimeField {...baseProps} />);
|
|
175
|
-
const input = container.querySelector('input[type="time"]') as HTMLInputElement;
|
|
176
|
-
fireEvent.change(input, { target: { value: '14:30' } });
|
|
177
|
-
expect(baseProps.onChange).toHaveBeenCalledWith('14:30');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('renders value directly in readonly mode', () => {
|
|
181
|
-
render(<TimeField {...baseProps} readonly value="14:30" />);
|
|
182
|
-
expect(screen.getByText('14:30')).toBeInTheDocument();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|
|
@@ -1,81 +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 widget id is a function of the (type, `multiple`) PAIR, not of the type
|
|
11
|
-
* string alone (objectui#3986).
|
|
12
|
-
*
|
|
13
|
-
* `mapFieldTypeToFormType` mapped `select` to `field:select` unconditionally,
|
|
14
|
-
* while `SelectField` delegated to `MultiSelectField` whenever the metadata said
|
|
15
|
-
* `multiple`. Three correct pieces, one gap between them: the component that
|
|
16
|
-
* RENDERED was `MultiSelectField`, but everything keyed on the widget id — above
|
|
17
|
-
* all the label-association declaration (`ComponentMeta.labelling`, resolved via
|
|
18
|
-
* `getMeta('select')`) — was still answering for the single-value combobox. The
|
|
19
|
-
* host label of a normal `{ type: 'select', multiple: true }` picklist therefore
|
|
20
|
-
* emitted a `for` at the chip row's wrapper `div`, where a `for` is inert.
|
|
21
|
-
*
|
|
22
|
-
* `select` cannot simply be declared `labelling: 'group'` instead: the
|
|
23
|
-
* single-value trigger IS labelable, and a bare `select` is a builtin the form
|
|
24
|
-
* renderer resolves without the registry at all (pinned in
|
|
25
|
-
* `form-group-label-association.test.ts`). The producer has to name the widget
|
|
26
|
-
* that renders, which is what this file pins.
|
|
27
|
-
*
|
|
28
|
-
* Metadata only — nothing renders here, so no widget module is loaded.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { describe, it, expect } from 'vitest';
|
|
32
|
-
import { mapFieldTypeToFormType } from './field-type-alias';
|
|
33
|
-
|
|
34
|
-
describe('select carries its arity in the widget id (objectui#3986)', () => {
|
|
35
|
-
it('maps select + multiple to the multi-value widget', () => {
|
|
36
|
-
// The one assertion that was `field:select` before this change.
|
|
37
|
-
expect(mapFieldTypeToFormType('select', { multiple: true })).toBe('field:multiselect');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('keeps the single-value widget without multiple, and for multiple: false', () => {
|
|
41
|
-
// The guard direction: single-select must not move. Its trigger is a
|
|
42
|
-
// labelable `button[role=combobox]` and its `for` association works — taking
|
|
43
|
-
// that away would break a field that is fine to fix one that is not.
|
|
44
|
-
expect(mapFieldTypeToFormType('select')).toBe('field:select');
|
|
45
|
-
expect(mapFieldTypeToFormType('select', {})).toBe('field:select');
|
|
46
|
-
expect(mapFieldTypeToFormType('select', { multiple: false })).toBe('field:select');
|
|
47
|
-
expect(mapFieldTypeToFormType('select', { multiple: null })).toBe('field:select');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('is idempotent for the type that is already inherently multi', () => {
|
|
51
|
-
// `multiselect` is a type in its own right; the arity override must not turn
|
|
52
|
-
// it into something else, whichever way the flag is set.
|
|
53
|
-
expect(mapFieldTypeToFormType('multiselect')).toBe('field:multiselect');
|
|
54
|
-
expect(mapFieldTypeToFormType('multiselect', { multiple: true })).toBe('field:multiselect');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The spec's multi-capable set is larger than the widget-moving set, and the
|
|
59
|
-
* difference is the whole reason this override is table-driven rather than a
|
|
60
|
-
* `config.multiple` branch sprinkled per type:
|
|
61
|
-
*
|
|
62
|
-
* `MULTI_CAPABLE_TYPES` = select / lookup / file / image (+ `radio` on the
|
|
63
|
-
* select branch, `user` storing like `lookup`)
|
|
64
|
-
*
|
|
65
|
-
* Every member except `select` renders BOTH arities inside one widget —
|
|
66
|
-
* `LookupField`, `FileField`, `ImageField` each read `multiple` themselves — so
|
|
67
|
-
* their widget id, and with it their `labelling` declaration, is correct for
|
|
68
|
-
* either arity. Moving them would point at widgets that do not exist.
|
|
69
|
-
*/
|
|
70
|
-
const UNMOVED_MULTI_CAPABLE = ['lookup', 'master_detail', 'user', 'owner', 'file', 'image', 'radio'] as const;
|
|
71
|
-
|
|
72
|
-
it.each(UNMOVED_MULTI_CAPABLE)('%s resolves identically with and without multiple', (type) => {
|
|
73
|
-
expect(mapFieldTypeToFormType(type, { multiple: true })).toBe(mapFieldTypeToFormType(type));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('does not invent a widget for an unknown type flagged multiple', () => {
|
|
77
|
-
// The fallback is unchanged: an unknown type is `field:text` at either arity,
|
|
78
|
-
// never `field:text-multiple`-anything.
|
|
79
|
-
expect(mapFieldTypeToFormType('something-unknown', { multiple: true })).toBe('field:text');
|
|
80
|
-
});
|
|
81
|
-
});
|
package/src/field-type-alias.ts
DELETED
|
@@ -1,156 +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 slice of a field definition the widget decision reads beyond `type`.
|
|
11
|
-
*
|
|
12
|
-
* Structural (not the spec `Field`) for the same reason the spec's own
|
|
13
|
-
* `ValueShapeFieldDef` is: every caller — object-schema metadata, a spec
|
|
14
|
-
* `FormFieldSchema` override, an action param def — can pass its own trimmed
|
|
15
|
-
* shape verbatim. `multiple` is the only key read; nothing else here is
|
|
16
|
-
* consulted, so a caller that has the whole metadata object may hand it over
|
|
17
|
-
* as-is.
|
|
18
|
-
*/
|
|
19
|
-
export interface FieldTypeMappingConfig {
|
|
20
|
-
/** `FieldSchema.multiple` — the field holds zero-or-more values. */
|
|
21
|
-
multiple?: boolean | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Field types whose `multiple: true` form is a DIFFERENT registered widget,
|
|
26
|
-
* rather than the same widget in another mode (objectui#3986).
|
|
27
|
-
*
|
|
28
|
-
* `select` is the only member, and the narrowness is measured, not assumed. The
|
|
29
|
-
* spec's `MULTI_CAPABLE_TYPES` is larger — select / lookup / file / image, with
|
|
30
|
-
* `radio` on the select branch and `user` storing like `lookup` — but every
|
|
31
|
-
* other member renders both arities INSIDE one widget: `LookupField`,
|
|
32
|
-
* `FileField` and `ImageField` each branch on `multiple` themselves, so their
|
|
33
|
-
* registry id, and with it their `labelling` declaration, is the same either
|
|
34
|
-
* way. A `select` declared `multiple: true` renders `MultiSelectField` instead:
|
|
35
|
-
* a different component, whose labelled surface is a chip row's container that
|
|
36
|
-
* a `<label for>` cannot address (it must be named by IDREF, which is what
|
|
37
|
-
* `field:multiselect`'s `labelling: 'group'` declares — objectui#3975/#3961).
|
|
38
|
-
*
|
|
39
|
-
* So the widget id has to carry the arity. When it did not, the host label was
|
|
40
|
-
* associated by the declaration registered under `select` — a single-value
|
|
41
|
-
* combobox that was NOT rendering — and the visible label of a normal
|
|
42
|
-
* `{ type: 'select', multiple: true }` picklist named nothing at all. One place
|
|
43
|
-
* decides which widget renders, so the declaration and the render cannot
|
|
44
|
-
* disagree again.
|
|
45
|
-
*/
|
|
46
|
-
const MULTI_VALUE_FORM_TYPES: Record<string, string> = {
|
|
47
|
-
select: 'field:multiselect',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Map field type to form component type
|
|
52
|
-
*
|
|
53
|
-
* @param fieldType - The ObjectQL field type identifier to convert
|
|
54
|
-
* (for example: `"text"`, `"number"`, `"date"`, `"lookup"`).
|
|
55
|
-
* @param config - The rest of the field definition, for the types whose widget
|
|
56
|
-
* identity depends on more than the type string. Only `multiple` is read; see
|
|
57
|
-
* {@link MULTI_VALUE_FORM_TYPES}. Omitting it maps the single-value form, which
|
|
58
|
-
* is what every non-multi-capable type resolves to anyway.
|
|
59
|
-
* @returns The normalized form field type string used in the form schema
|
|
60
|
-
* (for example: `"input"`, `"textarea"`, `"date-picker"`, `"select"`).
|
|
61
|
-
*/
|
|
62
|
-
export function mapFieldTypeToFormType(
|
|
63
|
-
fieldType: string,
|
|
64
|
-
config?: FieldTypeMappingConfig,
|
|
65
|
-
): string {
|
|
66
|
-
const typeMap: Record<string, string> = {
|
|
67
|
-
// Text-based fields
|
|
68
|
-
text: 'field:text',
|
|
69
|
-
textarea: 'field:textarea',
|
|
70
|
-
markdown: 'field:markdown', // Markdown editor (fallback to textarea)
|
|
71
|
-
html: 'field:html', // Rich text editor (fallback to textarea)
|
|
72
|
-
richtext: 'field:richtext', // WYSIWYG rich-text editor
|
|
73
|
-
secret: 'field:password', // encrypted-at-rest value — mask input like a password
|
|
74
|
-
|
|
75
|
-
// Numeric fields
|
|
76
|
-
number: 'field:number',
|
|
77
|
-
currency: 'field:currency',
|
|
78
|
-
percent: 'field:percent',
|
|
79
|
-
slider: 'field:slider',
|
|
80
|
-
progress: 'field:slider', // bounded 0..100 progress — edit via slider
|
|
81
|
-
rating: 'field:rating',
|
|
82
|
-
|
|
83
|
-
// Date/Time fields
|
|
84
|
-
date: 'field:date',
|
|
85
|
-
datetime: 'field:datetime',
|
|
86
|
-
time: 'field:time',
|
|
87
|
-
|
|
88
|
-
// Boolean
|
|
89
|
-
boolean: 'field:boolean',
|
|
90
|
-
toggle: 'field:boolean', // toggle is a boolean rendered as a switch
|
|
91
|
-
|
|
92
|
-
// Selection fields
|
|
93
|
-
select: 'field:select',
|
|
94
|
-
multiselect: 'field:multiselect',
|
|
95
|
-
radio: 'field:radio',
|
|
96
|
-
checkboxes: 'field:checkboxes',
|
|
97
|
-
tags: 'field:tags',
|
|
98
|
-
lookup: 'field:lookup',
|
|
99
|
-
master_detail: 'field:master_detail',
|
|
100
|
-
tree: 'field:lookup', // hierarchical reference — pick the parent via a lookup
|
|
101
|
-
// `user` is a lookup specialized to sys_user; `owner` mirrors it (record
|
|
102
|
-
// ownership). Both render via the UserField person-picker (delegates to the
|
|
103
|
-
// lookup picker). Without these they would fall through to `field:text`.
|
|
104
|
-
user: 'field:user',
|
|
105
|
-
owner: 'field:owner',
|
|
106
|
-
|
|
107
|
-
// Contact fields
|
|
108
|
-
email: 'field:email',
|
|
109
|
-
phone: 'field:phone',
|
|
110
|
-
url: 'field:url',
|
|
111
|
-
|
|
112
|
-
// File / media fields
|
|
113
|
-
file: 'field:file',
|
|
114
|
-
image: 'field:image',
|
|
115
|
-
avatar: 'field:avatar',
|
|
116
|
-
video: 'field:file', // uploads as a file
|
|
117
|
-
audio: 'field:file', // uploads as a file
|
|
118
|
-
signature: 'field:signature',
|
|
119
|
-
|
|
120
|
-
// Special / enhanced fields
|
|
121
|
-
password: 'field:password',
|
|
122
|
-
location: 'field:location', // Location/map field (fallback to input)
|
|
123
|
-
geolocation: 'field:geolocation',
|
|
124
|
-
address: 'field:address',
|
|
125
|
-
color: 'field:color',
|
|
126
|
-
code: 'field:code',
|
|
127
|
-
json: 'field:code', // JSON edited in the code editor
|
|
128
|
-
qrcode: 'field:qrcode',
|
|
129
|
-
vector: 'field:vector',
|
|
130
|
-
|
|
131
|
-
// Embedded structured values (stored as JSON on the row)
|
|
132
|
-
object: 'field:object',
|
|
133
|
-
composite: 'field:object', // embedded object
|
|
134
|
-
record: 'field:object', // name-keyed map
|
|
135
|
-
repeater: 'field:grid', // embedded array of rows
|
|
136
|
-
|
|
137
|
-
// Auto-generated/computed fields (typically read-only)
|
|
138
|
-
formula: 'field:formula',
|
|
139
|
-
summary: 'field:summary',
|
|
140
|
-
// `@objectstack/spec` spells this `autonumber`; the widget map key is
|
|
141
|
-
// `auto_number`. Map both spellings so a spec-typed field/param doesn't
|
|
142
|
-
// fall through to the plain text input.
|
|
143
|
-
autonumber: 'field:auto_number',
|
|
144
|
-
auto_number: 'field:auto_number',
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// The arity override comes FIRST and is table-driven: only a type listed in
|
|
148
|
-
// `MULTI_VALUE_FORM_TYPES` has a second widget to move to, so `multiple` on
|
|
149
|
-
// any other type (a multi `lookup`, a multi `file`) resolves exactly as
|
|
150
|
-
// before and keeps reaching the widget that handles both arities itself.
|
|
151
|
-
if (config?.multiple && MULTI_VALUE_FORM_TYPES[fieldType]) {
|
|
152
|
-
return MULTI_VALUE_FORM_TYPES[fieldType];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return typeMap[fieldType] || 'field:text';
|
|
156
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* Regression guard for the field-type → renderer registries. These mappings
|
|
6
|
-
* regressed once already: ~25 specialized field types silently fell back to a
|
|
7
|
-
* plain text input (form) and the raw text cell renderer (read), because the
|
|
8
|
-
* mapping tables were incomplete. This test locks them in so a dropped entry
|
|
9
|
-
* fails CI instead of shipping a broken widget.
|
|
10
|
-
*/
|
|
11
|
-
import { describe, it, expect } from 'vitest';
|
|
12
|
-
import { mapFieldTypeToFormType, getCellRenderer, TextCellRenderer } from './index';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Field types that MUST resolve to a dedicated form widget — never the
|
|
16
|
-
* `field:text` fallback. (Plain text-ish types like text/textarea are
|
|
17
|
-
* intentionally excluded.)
|
|
18
|
-
*/
|
|
19
|
-
const FORM_WIDGET_TYPES = [
|
|
20
|
-
'textarea', 'email', 'url', 'phone', 'password', 'secret',
|
|
21
|
-
'markdown', 'html', 'richtext',
|
|
22
|
-
'number', 'currency', 'percent', 'slider', 'progress', 'rating',
|
|
23
|
-
'date', 'datetime', 'time',
|
|
24
|
-
'boolean', 'toggle',
|
|
25
|
-
'select', 'multiselect', 'radio', 'checkboxes', 'tags',
|
|
26
|
-
'lookup', 'master_detail', 'tree',
|
|
27
|
-
'file', 'image', 'avatar', 'video', 'audio', 'signature',
|
|
28
|
-
'location', 'geolocation', 'address', 'color', 'code', 'json', 'qrcode', 'vector',
|
|
29
|
-
'object', 'composite', 'record', 'repeater',
|
|
30
|
-
// `autonumber` is the spec `FieldType` spelling; `auto_number` is the widget
|
|
31
|
-
// map key. Both must resolve to the AutoNumber widget, not text.
|
|
32
|
-
'formula', 'summary', 'auto_number', 'autonumber',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Field types that MUST resolve to a dedicated read/cell renderer — never the
|
|
37
|
-
* generic text cell renderer. (markdown/html/richtext render formatted;
|
|
38
|
-
* code/qrcode/time legitimately stay textual and are excluded.)
|
|
39
|
-
*/
|
|
40
|
-
const CELL_RENDERER_TYPES = [
|
|
41
|
-
'email', 'url', 'phone', 'number', 'currency', 'percent', 'progress',
|
|
42
|
-
'boolean', 'toggle', 'date', 'datetime',
|
|
43
|
-
'select', 'multiselect', 'radio', 'checkboxes', 'tags',
|
|
44
|
-
'lookup', 'master_detail', 'tree',
|
|
45
|
-
'file', 'video', 'audio', 'image', 'avatar', 'signature',
|
|
46
|
-
'markdown', 'html', 'richtext',
|
|
47
|
-
'location', 'geolocation', 'address', 'color', 'json',
|
|
48
|
-
'formula', 'summary', 'user', 'owner',
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
describe('field-type renderer coverage (regression guard)', () => {
|
|
52
|
-
it.each(FORM_WIDGET_TYPES)('maps "%s" to a dedicated form widget (not field:text)', (type) => {
|
|
53
|
-
const widget = mapFieldTypeToFormType(type);
|
|
54
|
-
expect(widget).toMatch(/^field:/);
|
|
55
|
-
expect(widget).not.toBe('field:text');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it.each(CELL_RENDERER_TYPES)('resolves "%s" to a dedicated cell renderer (not TextCellRenderer)', (type) => {
|
|
59
|
-
const renderer = getCellRenderer(type);
|
|
60
|
-
expect(renderer).toBeTypeOf('function');
|
|
61
|
-
expect(renderer).not.toBe(TextCellRenderer);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('falls back to field:text for unknown types', () => {
|
|
65
|
-
expect(mapFieldTypeToFormType('something-unknown')).toBe('field:text');
|
|
66
|
-
});
|
|
67
|
-
});
|