@object-ui/fields 6.2.2 → 7.0.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 +199 -0
- package/dist/complex-widgets.test.d.ts +1 -0
- package/dist/currency.d.ts +13 -0
- package/dist/datetime-widgets.test.d.ts +1 -0
- package/dist/field-type-coverage.test.d.ts +1 -0
- package/dist/index.cjs +4 -3
- package/dist/index.d.ts +300 -2
- package/dist/index.js +2460 -1274
- package/dist/standard-widgets.test.d.ts +0 -0
- package/dist/{packages/fields/src/widgets → widgets}/AddressField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/AutoNumberField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/AvatarField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/BooleanField.d.ts +2 -1
- package/dist/widgets/CheckboxesField.d.ts +8 -0
- package/dist/{packages/fields/src/widgets → widgets}/CodeField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/ColorField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/CurrencyField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/DateField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/DateTimeField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/EmailField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/FileField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/FormulaField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/GeolocationField.d.ts +2 -1
- package/dist/widgets/GridField.d.ts +123 -0
- package/dist/widgets/GridField.test.d.ts +1 -0
- package/dist/{packages/fields/src/widgets → widgets}/ImageField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/LocationField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/LookupField.d.ts +2 -1
- package/dist/widgets/MarkdownContent.d.ts +9 -0
- package/dist/{packages/fields/src/widgets → widgets}/MasterDetailField.d.ts +2 -1
- package/dist/widgets/MultiSelectField.d.ts +9 -0
- package/dist/{packages/fields/src/widgets → widgets}/NumberField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/ObjectField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/PasswordField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/PercentField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/PhoneField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/QRCodeField.d.ts +2 -1
- package/dist/widgets/RadioField.d.ts +8 -0
- package/dist/{packages/fields/src/widgets → widgets}/RatingField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/RecordPickerDialog.d.ts +8 -2
- package/dist/{packages/fields/src/widgets → widgets}/RichTextField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/SelectField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/SignatureField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/SliderField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/SummaryField.d.ts +2 -1
- package/dist/widgets/TagsField.d.ts +8 -0
- package/dist/{packages/fields/src/widgets → widgets}/TextAreaField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/TextField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/TimeField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/UrlField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/UserField.d.ts +2 -1
- package/dist/{packages/fields/src/widgets → widgets}/VectorField.d.ts +2 -1
- package/dist/widgets/deriveLookupColumns.d.ts +36 -0
- package/dist/widgets/deriveLookupColumns.test.d.ts +1 -0
- package/dist/widgets/recentLookups.d.ts +13 -0
- package/dist/widgets/recentLookups.test.d.ts +1 -0
- package/dist/{packages/fields/src/widgets → widgets}/types.d.ts +1 -1
- package/package.json +14 -11
- package/src/__tests__/registry-collision.test.ts +91 -0
- package/src/__tests__/resolveFieldCurrency.test.ts +20 -0
- package/src/coerce-safe-value.test.ts +34 -0
- package/src/complex-widgets.test.tsx +58 -7
- package/src/currency.ts +14 -0
- package/src/field-type-coverage.test.ts +65 -0
- package/src/index.tsx +372 -57
- package/src/widgets/CheckboxesField.tsx +53 -0
- package/src/widgets/CurrencyField.tsx +5 -1
- package/src/widgets/GridField.test.tsx +309 -0
- package/src/widgets/GridField.tsx +907 -33
- package/src/widgets/LookupField.tsx +258 -107
- package/src/widgets/MarkdownContent.tsx +19 -0
- package/src/widgets/MultiSelectField.tsx +59 -0
- package/src/widgets/PercentField.tsx +19 -7
- package/src/widgets/RadioField.tsx +41 -0
- package/src/widgets/RecordPickerDialog.tsx +22 -18
- package/src/widgets/SelectField.tsx +23 -2
- package/src/widgets/TagsField.tsx +67 -0
- package/src/widgets/deriveLookupColumns.test.ts +74 -0
- package/src/widgets/deriveLookupColumns.ts +104 -0
- package/src/widgets/recentLookups.test.ts +45 -0
- package/src/widgets/recentLookups.ts +47 -0
- package/src/widgets/useFieldTranslation.ts +30 -0
- package/dist/packages/fields/src/fields-edge-cases.stories.d.ts +0 -45
- package/dist/packages/fields/src/fields.stories.d.ts +0 -56
- package/dist/packages/fields/src/index.d.ts +0 -286
- package/dist/packages/fields/src/record-picker-dialog.stories.d.ts +0 -25
- package/dist/packages/fields/src/widgets/GridField.d.ts +0 -7
- package/src/fields-edge-cases.stories.tsx +0 -407
- package/src/fields.stories.tsx +0 -566
- package/src/record-picker-dialog.stories.tsx +0 -160
- /package/dist/{packages/fields/src/__tests__/validation-feedback.test.d.ts → __tests__/registry-collision.test.d.ts} +0 -0
- /package/dist/{packages/fields/src/complex-widgets.test.d.ts → __tests__/resolveFieldCurrency.test.d.ts} +0 -0
- /package/dist/{packages/fields/src/standard-widgets.test.d.ts → __tests__/validation-feedback.test.d.ts} +0 -0
- /package/dist/{packages/fields/src/datetime-widgets.test.d.ts → coerce-safe-value.test.d.ts} +0 -0
- /package/dist/{packages/fields/src/widgets → widgets}/ImageCropperDialog.d.ts +0 -0
- /package/dist/{packages/fields/src/widgets → widgets}/_cell-renderer-bridge.d.ts +0 -0
- /package/dist/{packages/fields/src/widgets → widgets}/useFieldTranslation.d.ts +0 -0
package/src/index.tsx
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import type { FieldMetadata, SelectOptionMetadata } from '@object-ui/types';
|
|
11
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
11
|
+
import { ComponentRegistry, percentDisplayValue } from '@object-ui/core';
|
|
12
|
+
import { useLocalization } from '@object-ui/i18n';
|
|
12
13
|
import { Badge, Avatar, AvatarFallback, Button, Checkbox, EmptyValue, cn } from '@object-ui/components';
|
|
13
|
-
import { Check, X, Copy, Phone as PhoneIcon } from 'lucide-react';
|
|
14
|
+
import { Check, X, Copy, Phone as PhoneIcon, MapPin } from 'lucide-react';
|
|
14
15
|
import { useObjectTranslation } from '@object-ui/react';
|
|
15
16
|
import { SchemaRendererContext as _SchemaRendererContext } from '@object-ui/react';
|
|
16
17
|
|
|
@@ -222,13 +223,23 @@ export interface CellRendererProps {
|
|
|
222
223
|
*/
|
|
223
224
|
export function coerceToSafeValue(value: unknown): string | number | boolean | null | undefined {
|
|
224
225
|
if (value == null) return value as null | undefined;
|
|
225
|
-
if (typeof value === '
|
|
226
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
227
|
+
if (typeof value === 'string') {
|
|
228
|
+
// A reference/expanded value can arrive as a JSON-encoded object string —
|
|
229
|
+
// e.g. an unresolved external-id reference '{"externalId":"Website Relaunch"}'.
|
|
230
|
+
// Parse and extract a human label instead of leaking raw JSON into the cell.
|
|
231
|
+
const s = value.trim();
|
|
232
|
+
if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('[') && s.endsWith(']'))) {
|
|
233
|
+
try { return coerceToSafeValue(JSON.parse(s)); } catch { /* not JSON — fall through */ }
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
226
237
|
if (value instanceof Date) return value.toISOString();
|
|
227
238
|
if (Array.isArray(value)) {
|
|
228
239
|
return value.map((v) => {
|
|
229
240
|
if (v != null && typeof v === 'object') {
|
|
230
241
|
const obj = v as Record<string, unknown>;
|
|
231
|
-
return String(obj.name || obj.label || obj.id || obj._id || '[Object]');
|
|
242
|
+
return String(obj.name || obj.label || obj.externalId || obj.id || obj._id || '[Object]');
|
|
232
243
|
}
|
|
233
244
|
return String(v);
|
|
234
245
|
}).join(', ');
|
|
@@ -241,8 +252,8 @@ export function coerceToSafeValue(value: unknown): string | number | boolean | n
|
|
|
241
252
|
if ('$oid' in obj) return String(obj.$oid);
|
|
242
253
|
// MongoDB date wrapper: { $date: "2024-01-01T00:00:00Z" }
|
|
243
254
|
if ('$date' in obj) return String(obj.$date);
|
|
244
|
-
// Expanded reference / general object: extract name/label/id
|
|
245
|
-
return String(obj.name || obj.label || obj.id || obj._id || '[Object]');
|
|
255
|
+
// Expanded reference / general object: extract name/label/externalId/id
|
|
256
|
+
return String(obj.name || obj.label || obj.externalId || obj.id || obj._id || '[Object]');
|
|
246
257
|
}
|
|
247
258
|
return String(value);
|
|
248
259
|
}
|
|
@@ -256,6 +267,9 @@ export function coerceToSafeValue(value: unknown): string | number | boolean | n
|
|
|
256
267
|
* Trailing `.00` is dropped when the value is a whole number — Salesforce
|
|
257
268
|
* convention: `$1,234.50` keeps cents; `$1,234` does not.
|
|
258
269
|
*/
|
|
270
|
+
import { resolveFieldCurrency } from './currency';
|
|
271
|
+
export { resolveFieldCurrency };
|
|
272
|
+
|
|
259
273
|
export function formatCurrency(value: number, currency?: string): string {
|
|
260
274
|
const isWhole = Number.isFinite(value) && value === Math.trunc(value);
|
|
261
275
|
const maxFrac = isWhole ? 0 : 2;
|
|
@@ -327,8 +341,9 @@ export function formatNumber(value: number, decimals: number = 2): string {
|
|
|
327
341
|
* Handles both decimal (0.8 = 80%) and whole number (80 = 80%) inputs.
|
|
328
342
|
*/
|
|
329
343
|
export function formatPercent(value: number, precision: number = 0): string {
|
|
330
|
-
//
|
|
331
|
-
|
|
344
|
+
// Scale a fraction-stored percent (0.8 → 80%) via the shared core helper, so
|
|
345
|
+
// the list cell and the dashboard measure formatter (`formatMeasure`) agree.
|
|
346
|
+
const displayValue = percentDisplayValue(value);
|
|
332
347
|
return `${displayValue.toFixed(precision)}%`;
|
|
333
348
|
}
|
|
334
349
|
|
|
@@ -454,18 +469,12 @@ export function CurrencyCellRenderer({ value, field }: CellRendererProps): React
|
|
|
454
469
|
if (value == null) return <EmptyValue />;
|
|
455
470
|
|
|
456
471
|
const safe = coerceToSafeValue(value);
|
|
457
|
-
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// misleading for non-USD orgs (e.g. RMB amounts displayed as $).
|
|
464
|
-
const currency: string | undefined =
|
|
465
|
-
currencyField.currency ||
|
|
466
|
-
currencyField.defaultCurrency ||
|
|
467
|
-
currencyField.currencyConfig?.defaultCurrency ||
|
|
468
|
-
undefined;
|
|
472
|
+
// Resolve the display currency via the shared precedence: field `currency` →
|
|
473
|
+
// `currencyConfig.defaultCurrency` → the tenant default (ADR-0053). When none
|
|
474
|
+
// is known, render a plain number — never a guessed symbol (silently assuming
|
|
475
|
+
// USD mis-displays non-USD orgs, e.g. RMB amounts shown as $).
|
|
476
|
+
const { currency: tenantCurrency } = useLocalization();
|
|
477
|
+
const currency = resolveFieldCurrency(field as any, tenantCurrency);
|
|
469
478
|
const num = Number(safe);
|
|
470
479
|
const formatted = !isNaN(num)
|
|
471
480
|
? formatCurrency(num, currency)
|
|
@@ -769,8 +778,52 @@ function hashToColor(value: string): string {
|
|
|
769
778
|
return BADGE_FALLBACK_PALETTE[idx];
|
|
770
779
|
}
|
|
771
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Map a hex color (e.g. '#8B5CF6') to the nearest named palette color the
|
|
783
|
+
* badge/dot maps understand. Object field options almost always declare colors
|
|
784
|
+
* as HEX, so without this the explicit author color is ignored and a semantic/
|
|
785
|
+
* hash heuristic takes over (e.g. a purple 'In Review' rendered alarming-red).
|
|
786
|
+
* Low-saturation hexes resolve to 'gray'; otherwise bucket by hue.
|
|
787
|
+
*/
|
|
788
|
+
function hexToPaletteName(hex: string): string | undefined {
|
|
789
|
+
const m = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
|
|
790
|
+
if (!m) return undefined;
|
|
791
|
+
let h = m[1];
|
|
792
|
+
if (h.length === 3) h = h.split('').map((c) => c + c).join('');
|
|
793
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
794
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
795
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
796
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
|
|
797
|
+
const l = (max + min) / 2;
|
|
798
|
+
const sat = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
|
|
799
|
+
if (sat < 0.22) return 'gray';
|
|
800
|
+
let hue: number;
|
|
801
|
+
if (max === r) hue = (((g - b) / d) % 6 + 6) % 6;
|
|
802
|
+
else if (max === g) hue = (b - r) / d + 2;
|
|
803
|
+
else hue = (r - g) / d + 4;
|
|
804
|
+
hue *= 60;
|
|
805
|
+
if (hue >= 345 || hue < 15) return 'red';
|
|
806
|
+
if (hue < 30) return 'orange';
|
|
807
|
+
if (hue < 60) return 'yellow';
|
|
808
|
+
if (hue < 170) return 'green';
|
|
809
|
+
if (hue < 238) return 'blue';
|
|
810
|
+
if (hue < 250) return 'indigo';
|
|
811
|
+
if (hue < 295) return 'purple';
|
|
812
|
+
return 'pink';
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Normalize an option color to a named palette key: pass known names through,
|
|
816
|
+
* resolve hex to the nearest palette color, else undefined. */
|
|
817
|
+
function resolveColorName(color?: string): string | undefined {
|
|
818
|
+
if (!color) return undefined;
|
|
819
|
+
if (BADGE_COLOR_MAP[color]) return color;
|
|
820
|
+
if (color.charAt(0) === '#') return hexToPaletteName(color);
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
|
|
772
824
|
export function getBadgeColorClasses(color?: string, val?: unknown): string {
|
|
773
|
-
|
|
825
|
+
const named = resolveColorName(color);
|
|
826
|
+
if (named && BADGE_COLOR_MAP[named]) return BADGE_COLOR_MAP[named];
|
|
774
827
|
if (val == null || val === '') return 'bg-muted text-muted-foreground border-border';
|
|
775
828
|
const key = String(val).toLowerCase().replace(/[\s-]/g, '_');
|
|
776
829
|
const semantic = SEMANTIC_COLOR_MAP[key];
|
|
@@ -790,7 +843,8 @@ export function getBadgeColorClasses(color?: string, val?: unknown): string {
|
|
|
790
843
|
* supplied so the caller can fall back to its own default.
|
|
791
844
|
*/
|
|
792
845
|
export function getSemanticColorName(color?: string, val?: unknown): string | undefined {
|
|
793
|
-
|
|
846
|
+
const named = resolveColorName(color);
|
|
847
|
+
if (named && BADGE_COLOR_MAP[named]) return named;
|
|
794
848
|
if (val == null || val === '') return undefined;
|
|
795
849
|
const key = String(val).toLowerCase().replace(/[\s-]/g, '_');
|
|
796
850
|
const semantic = SEMANTIC_COLOR_MAP[key];
|
|
@@ -862,7 +916,7 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
|
|
|
862
916
|
if (appearance === 'dot') {
|
|
863
917
|
// Resolve a real CSS color for the dot. Prefer explicit option color,
|
|
864
918
|
// then semantic mapping for the value, then deterministic palette.
|
|
865
|
-
const colorName = option?.color
|
|
919
|
+
const colorName = resolveColorName(option?.color)
|
|
866
920
|
|| SEMANTIC_COLOR_MAP[String(val).toLowerCase().replace(/[\s-]/g, '_')]
|
|
867
921
|
|| hashToColor(String(val).toLowerCase().replace(/[\s-]/g, '_'));
|
|
868
922
|
const dotClass = DOT_COLOR_MAP[colorName] || DOT_COLOR_MAP.gray;
|
|
@@ -1044,31 +1098,44 @@ export function FileCellRenderer({ value, field }: CellRendererProps): React.Rea
|
|
|
1044
1098
|
*/
|
|
1045
1099
|
export function ImageCellRenderer({ value }: CellRendererProps): React.ReactElement {
|
|
1046
1100
|
if (!value) return <EmptyValue />;
|
|
1047
|
-
|
|
1101
|
+
|
|
1102
|
+
// An image value may be a plain URL string, an object ({ url | src | href }),
|
|
1103
|
+
// or an array of either. Normalize so a string-URL field (common — e.g. a
|
|
1104
|
+
// `cover` seeded with a CDN link) renders a thumbnail instead of a broken
|
|
1105
|
+
// <img src=""> placeholder.
|
|
1106
|
+
const urlOf = (v: any): string =>
|
|
1107
|
+
typeof v === 'string' ? v : (v?.url || v?.src || v?.href || v?.thumbnailUrl || '');
|
|
1108
|
+
const nameOf = (v: any, fallback: string): string =>
|
|
1109
|
+
(v && typeof v === 'object' && (v.name || v.original_name)) || fallback;
|
|
1110
|
+
|
|
1048
1111
|
if (Array.isArray(value)) {
|
|
1112
|
+
const imgs = value.map((v) => ({ url: urlOf(v), name: nameOf(v, 'Image') })).filter((i) => i.url);
|
|
1113
|
+
if (imgs.length === 0) return <EmptyValue />;
|
|
1049
1114
|
return (
|
|
1050
1115
|
<div className="flex -space-x-2">
|
|
1051
|
-
{
|
|
1116
|
+
{imgs.slice(0, 3).map((img, idx) => (
|
|
1052
1117
|
<img
|
|
1053
1118
|
key={idx}
|
|
1054
|
-
src={img.url
|
|
1119
|
+
src={img.url}
|
|
1055
1120
|
alt={img.name || `Image ${idx + 1}`}
|
|
1056
1121
|
className="size-8 rounded-md border-2 border-white object-cover"
|
|
1057
1122
|
/>
|
|
1058
1123
|
))}
|
|
1059
|
-
{
|
|
1124
|
+
{imgs.length > 3 && (
|
|
1060
1125
|
<div className="size-8 rounded-md border-2 border-white bg-gray-100 flex items-center justify-center text-xs font-medium text-gray-600">
|
|
1061
|
-
+{
|
|
1126
|
+
+{imgs.length - 3}
|
|
1062
1127
|
</div>
|
|
1063
1128
|
)}
|
|
1064
1129
|
</div>
|
|
1065
1130
|
);
|
|
1066
1131
|
}
|
|
1067
|
-
|
|
1132
|
+
|
|
1133
|
+
const url = urlOf(value);
|
|
1134
|
+
if (!url) return <EmptyValue />;
|
|
1068
1135
|
return (
|
|
1069
1136
|
<img
|
|
1070
|
-
src={
|
|
1071
|
-
alt={value
|
|
1137
|
+
src={url}
|
|
1138
|
+
alt={nameOf(value, 'Image')}
|
|
1072
1139
|
className="size-10 rounded-md object-cover"
|
|
1073
1140
|
/>
|
|
1074
1141
|
);
|
|
@@ -1112,6 +1179,24 @@ export function LookupCellRenderer({ value, field }: CellRendererProps): React.R
|
|
|
1112
1179
|
|
|
1113
1180
|
if (value == null || value === '') return <EmptyValue />;
|
|
1114
1181
|
|
|
1182
|
+
// A reference can arrive as a JSON-encoded object string — e.g. an
|
|
1183
|
+
// unresolved external-id reference '{"externalId":"Website Relaunch"}'.
|
|
1184
|
+
// Parse it and render a label instead of leaking raw JSON into the cell.
|
|
1185
|
+
if (typeof value === 'string') {
|
|
1186
|
+
const s = value.trim();
|
|
1187
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
1188
|
+
try {
|
|
1189
|
+
const parsed = JSON.parse(s) as Record<string, unknown>;
|
|
1190
|
+
if (parsed && typeof parsed === 'object') {
|
|
1191
|
+
const display =
|
|
1192
|
+
pickRecordDisplayName(parsed) ||
|
|
1193
|
+
String(parsed.externalId ?? parsed.id ?? parsed._id ?? '');
|
|
1194
|
+
if (display) return <span className="truncate">{display}</span>;
|
|
1195
|
+
}
|
|
1196
|
+
} catch { /* not JSON — fall through to normal resolution */ }
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1115
1200
|
// Server-side $expand returns the related record as a nested object
|
|
1116
1201
|
// (e.g. { id, name }). Render its display name directly — no fetch needed.
|
|
1117
1202
|
if (!Array.isArray(value) && typeof value === 'object') {
|
|
@@ -1323,6 +1408,117 @@ export function resolveCellRendererType(fieldOrType: string | { type?: string; f
|
|
|
1323
1408
|
return fieldOrType.type || 'text';
|
|
1324
1409
|
}
|
|
1325
1410
|
|
|
1411
|
+
/**
|
|
1412
|
+
* Renders structured/embedded values (json, object, composite, record,
|
|
1413
|
+
* address, geolocation) as compact, readable JSON. Objects and arrays are
|
|
1414
|
+
* stringified; primitives fall through to their string form.
|
|
1415
|
+
*/
|
|
1416
|
+
export function JsonCellRenderer({ value }: CellRendererProps): React.ReactElement {
|
|
1417
|
+
if (value == null || value === '') return <EmptyValue />;
|
|
1418
|
+
let text: string;
|
|
1419
|
+
if (typeof value === 'object') {
|
|
1420
|
+
try {
|
|
1421
|
+
text = JSON.stringify(value);
|
|
1422
|
+
} catch {
|
|
1423
|
+
text = String(value);
|
|
1424
|
+
}
|
|
1425
|
+
} else {
|
|
1426
|
+
text = String(value);
|
|
1427
|
+
}
|
|
1428
|
+
return <span className="font-mono text-xs text-gray-600 truncate" title={text}>{text}</span>;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Renders a `color` value as a swatch alongside its hex/string value.
|
|
1433
|
+
*/
|
|
1434
|
+
export function ColorSwatchCellRenderer({ value }: CellRendererProps): React.ReactElement {
|
|
1435
|
+
if (value == null || value === '') return <EmptyValue />;
|
|
1436
|
+
const color = String(value);
|
|
1437
|
+
return (
|
|
1438
|
+
<span className="inline-flex items-center gap-1.5 text-sm">
|
|
1439
|
+
<span
|
|
1440
|
+
className="h-3.5 w-3.5 rounded border border-black/10 shrink-0"
|
|
1441
|
+
style={{ backgroundColor: color }}
|
|
1442
|
+
aria-hidden="true"
|
|
1443
|
+
/>
|
|
1444
|
+
<span className="font-mono text-xs">{color}</span>
|
|
1445
|
+
</span>
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const LazyMarkdownContent = React.lazy(() => import('./widgets/MarkdownContent'));
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* Renders `markdown` / `richtext` values as formatted GFM markdown (lazy-loaded,
|
|
1453
|
+
* sanitized) instead of the raw markup string.
|
|
1454
|
+
*/
|
|
1455
|
+
export function MarkdownCellRenderer({ value }: CellRendererProps): React.ReactElement {
|
|
1456
|
+
if (value == null || value === '') return <EmptyValue />;
|
|
1457
|
+
return (
|
|
1458
|
+
<React.Suspense fallback={<span className="text-sm text-muted-foreground">{String(value).slice(0, 80)}</span>}>
|
|
1459
|
+
<LazyMarkdownContent value={String(value)} />
|
|
1460
|
+
</React.Suspense>
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Minimal HTML sanitizer for display: drops <script>/<style>/<iframe> blocks,
|
|
1466
|
+
* inline event handlers, and javascript: URLs. Defense-in-depth — stored HTML
|
|
1467
|
+
* is authored by users with write access, but is never trusted blindly.
|
|
1468
|
+
*/
|
|
1469
|
+
function sanitizeHtml(html: string): string {
|
|
1470
|
+
return html
|
|
1471
|
+
.replace(/<\s*(script|style|iframe|object|embed)[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, '')
|
|
1472
|
+
.replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
|
1473
|
+
.replace(/(href|src)\s*=\s*("javascript:[^"]*"|'javascript:[^']*')/gi, '$1="#"');
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Renders an `html` value as sanitized, formatted HTML instead of raw markup.
|
|
1478
|
+
*/
|
|
1479
|
+
export function HtmlCellRenderer({ value }: CellRendererProps): React.ReactElement {
|
|
1480
|
+
if (value == null || value === '') return <EmptyValue />;
|
|
1481
|
+
return (
|
|
1482
|
+
<div
|
|
1483
|
+
className="prose prose-sm max-w-none dark:prose-invert break-words"
|
|
1484
|
+
dangerouslySetInnerHTML={{ __html: sanitizeHtml(String(value)) }}
|
|
1485
|
+
/>
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Renders a `location`/`geolocation` value as readable coordinates with a pin.
|
|
1491
|
+
* Accepts `{ lat, lng }` / `{ latitude, longitude }`, a `"lat,lng"` string,
|
|
1492
|
+
* or a `[lat, lng]` array. Falls back to compact JSON for anything else.
|
|
1493
|
+
*/
|
|
1494
|
+
export function LocationCellRenderer({ value }: CellRendererProps): React.ReactElement {
|
|
1495
|
+
if (value == null || value === '') return <EmptyValue />;
|
|
1496
|
+
let lat: number | undefined;
|
|
1497
|
+
let lng: number | undefined;
|
|
1498
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
1499
|
+
const v = value as Record<string, any>;
|
|
1500
|
+
lat = typeof v.lat === 'number' ? v.lat : typeof v.latitude === 'number' ? v.latitude : undefined;
|
|
1501
|
+
lng = typeof v.lng === 'number' ? v.lng : typeof v.lon === 'number' ? v.lon : typeof v.longitude === 'number' ? v.longitude : undefined;
|
|
1502
|
+
} else if (Array.isArray(value) && value.length === 2) {
|
|
1503
|
+
lat = Number(value[0]);
|
|
1504
|
+
lng = Number(value[1]);
|
|
1505
|
+
} else if (typeof value === 'string') {
|
|
1506
|
+
const parts = value.split(',').map((s) => parseFloat(s.trim()));
|
|
1507
|
+
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
|
|
1508
|
+
[lat, lng] = parts;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (typeof lat === 'number' && typeof lng === 'number' && !isNaN(lat) && !isNaN(lng)) {
|
|
1512
|
+
return (
|
|
1513
|
+
<span className="inline-flex items-center gap-1 text-sm tabular-nums">
|
|
1514
|
+
<MapPin className="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
1515
|
+
{lat.toFixed(4)}, {lng.toFixed(4)}
|
|
1516
|
+
</span>
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
return <JsonCellRenderer value={value} field={{} as any} />;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1326
1522
|
/**
|
|
1327
1523
|
* Get the appropriate cell renderer for a field type
|
|
1328
1524
|
*/
|
|
@@ -1336,32 +1532,61 @@ export function getCellRenderer(fieldType: string): React.FC<CellRendererProps>
|
|
|
1336
1532
|
const standardMap: Record<string, React.FC<CellRendererProps>> = {
|
|
1337
1533
|
text: TextCellRenderer,
|
|
1338
1534
|
textarea: TextCellRenderer,
|
|
1339
|
-
markdown:
|
|
1340
|
-
html:
|
|
1535
|
+
markdown: MarkdownCellRenderer,
|
|
1536
|
+
html: HtmlCellRenderer,
|
|
1537
|
+
richtext: MarkdownCellRenderer,
|
|
1538
|
+
code: TextCellRenderer,
|
|
1539
|
+
qrcode: TextCellRenderer,
|
|
1341
1540
|
number: NumberCellRenderer,
|
|
1342
1541
|
currency: CurrencyCellRenderer,
|
|
1343
1542
|
percent: PercentCellRenderer,
|
|
1543
|
+
progress: PercentCellRenderer,
|
|
1544
|
+
slider: NumberCellRenderer,
|
|
1545
|
+
rating: NumberCellRenderer,
|
|
1344
1546
|
boolean: BooleanCellRenderer,
|
|
1547
|
+
toggle: BooleanCellRenderer,
|
|
1345
1548
|
date: DateCellRenderer,
|
|
1346
1549
|
datetime: DateTimeCellRenderer,
|
|
1347
1550
|
time: TextCellRenderer,
|
|
1348
1551
|
select: SelectCellRenderer,
|
|
1349
1552
|
status: SelectCellRenderer,
|
|
1553
|
+
multiselect: SelectCellRenderer,
|
|
1554
|
+
radio: SelectCellRenderer,
|
|
1555
|
+
checkboxes: SelectCellRenderer,
|
|
1556
|
+
tags: SelectCellRenderer,
|
|
1350
1557
|
lookup: LookupCellRenderer,
|
|
1351
1558
|
master_detail: LookupCellRenderer,
|
|
1559
|
+
tree: LookupCellRenderer,
|
|
1352
1560
|
email: EmailCellRenderer,
|
|
1353
1561
|
url: UrlCellRenderer,
|
|
1354
1562
|
phone: PhoneCellRenderer,
|
|
1355
1563
|
file: FileCellRenderer,
|
|
1564
|
+
video: FileCellRenderer,
|
|
1565
|
+
audio: FileCellRenderer,
|
|
1356
1566
|
image: ImageCellRenderer,
|
|
1567
|
+
avatar: ImageCellRenderer,
|
|
1568
|
+
signature: ImageCellRenderer,
|
|
1357
1569
|
formula: FormulaCellRenderer,
|
|
1358
1570
|
summary: FormulaCellRenderer,
|
|
1359
1571
|
auto_number: TextCellRenderer,
|
|
1360
1572
|
user: UserCellRenderer,
|
|
1361
1573
|
owner: UserCellRenderer,
|
|
1362
1574
|
password: () => <span>••••••</span>,
|
|
1363
|
-
|
|
1364
|
-
|
|
1575
|
+
secret: () => <span>••••••</span>,
|
|
1576
|
+
location: LocationCellRenderer,
|
|
1577
|
+
geolocation: LocationCellRenderer,
|
|
1578
|
+
address: JsonCellRenderer,
|
|
1579
|
+
color: ColorSwatchCellRenderer,
|
|
1580
|
+
json: JsonCellRenderer,
|
|
1581
|
+
object: JsonCellRenderer,
|
|
1582
|
+
composite: JsonCellRenderer,
|
|
1583
|
+
record: JsonCellRenderer,
|
|
1584
|
+
repeater: ({ value }: CellRendererProps) => {
|
|
1585
|
+
const n = Array.isArray(value) ? value.length : 0;
|
|
1586
|
+
return n > 0
|
|
1587
|
+
? <span className="text-gray-500 italic">{n} 项</span>
|
|
1588
|
+
: <EmptyValue />;
|
|
1589
|
+
},
|
|
1365
1590
|
vector: () => <span className="text-gray-500 italic">[Vector]</span>,
|
|
1366
1591
|
grid: () => <span className="text-gray-500 italic">[Grid]</span>,
|
|
1367
1592
|
};
|
|
@@ -1402,38 +1627,66 @@ export function mapFieldTypeToFormType(fieldType: string): string {
|
|
|
1402
1627
|
textarea: 'field:textarea',
|
|
1403
1628
|
markdown: 'field:markdown', // Markdown editor (fallback to textarea)
|
|
1404
1629
|
html: 'field:html', // Rich text editor (fallback to textarea)
|
|
1405
|
-
|
|
1630
|
+
richtext: 'field:richtext', // WYSIWYG rich-text editor
|
|
1631
|
+
secret: 'field:password', // encrypted-at-rest value — mask input like a password
|
|
1632
|
+
|
|
1406
1633
|
// Numeric fields
|
|
1407
1634
|
number: 'field:number',
|
|
1408
1635
|
currency: 'field:currency',
|
|
1409
1636
|
percent: 'field:percent',
|
|
1410
|
-
|
|
1637
|
+
slider: 'field:slider',
|
|
1638
|
+
progress: 'field:slider', // bounded 0..100 progress — edit via slider
|
|
1639
|
+
rating: 'field:rating',
|
|
1640
|
+
|
|
1411
1641
|
// Date/Time fields
|
|
1412
1642
|
date: 'field:date',
|
|
1413
1643
|
datetime: 'field:datetime',
|
|
1414
1644
|
time: 'field:time',
|
|
1415
|
-
|
|
1645
|
+
|
|
1416
1646
|
// Boolean
|
|
1417
1647
|
boolean: 'field:boolean',
|
|
1418
|
-
|
|
1648
|
+
toggle: 'field:boolean', // toggle is a boolean rendered as a switch
|
|
1649
|
+
|
|
1419
1650
|
// Selection fields
|
|
1420
1651
|
select: 'field:select',
|
|
1652
|
+
multiselect: 'field:multiselect',
|
|
1653
|
+
radio: 'field:radio',
|
|
1654
|
+
checkboxes: 'field:checkboxes',
|
|
1655
|
+
tags: 'field:tags',
|
|
1421
1656
|
lookup: 'field:lookup',
|
|
1422
1657
|
master_detail: 'field:master_detail',
|
|
1423
|
-
|
|
1658
|
+
tree: 'field:lookup', // hierarchical reference — pick the parent via a lookup
|
|
1659
|
+
|
|
1424
1660
|
// Contact fields
|
|
1425
1661
|
email: 'field:email',
|
|
1426
1662
|
phone: 'field:phone',
|
|
1427
1663
|
url: 'field:url',
|
|
1428
|
-
|
|
1429
|
-
// File fields
|
|
1664
|
+
|
|
1665
|
+
// File / media fields
|
|
1430
1666
|
file: 'field:file',
|
|
1431
1667
|
image: 'field:image',
|
|
1432
|
-
|
|
1433
|
-
//
|
|
1668
|
+
avatar: 'field:avatar',
|
|
1669
|
+
video: 'field:file', // uploads as a file
|
|
1670
|
+
audio: 'field:file', // uploads as a file
|
|
1671
|
+
signature: 'field:signature',
|
|
1672
|
+
|
|
1673
|
+
// Special / enhanced fields
|
|
1434
1674
|
password: 'field:password',
|
|
1435
1675
|
location: 'field:location', // Location/map field (fallback to input)
|
|
1436
|
-
|
|
1676
|
+
geolocation: 'field:geolocation',
|
|
1677
|
+
address: 'field:address',
|
|
1678
|
+
color: 'field:color',
|
|
1679
|
+
code: 'field:code',
|
|
1680
|
+
json: 'field:code', // JSON edited in the code editor
|
|
1681
|
+
qrcode: 'field:qrcode',
|
|
1682
|
+
vector: 'field:vector',
|
|
1683
|
+
|
|
1684
|
+
// Embedded structured values (stored as JSON on the row)
|
|
1685
|
+
object: 'field:object',
|
|
1686
|
+
composite: 'field:object', // embedded object
|
|
1687
|
+
record: 'field:object', // name-keyed map
|
|
1688
|
+
repeater: 'field:grid', // embedded array of rows
|
|
1689
|
+
|
|
1437
1690
|
// Auto-generated/computed fields (typically read-only)
|
|
1438
1691
|
formula: 'field:formula',
|
|
1439
1692
|
summary: 'field:summary',
|
|
@@ -1665,12 +1918,19 @@ const fieldWidgetMap: Record<string, () => Promise<{ default: React.ComponentTyp
|
|
|
1665
1918
|
'phone': () => import('./widgets/PhoneField').then(m => ({ default: m.PhoneField })),
|
|
1666
1919
|
'url': () => import('./widgets/UrlField').then(m => ({ default: m.UrlField })),
|
|
1667
1920
|
|
|
1921
|
+
// Selection fields (multi-value / option groups)
|
|
1922
|
+
'multiselect': () => import('./widgets/MultiSelectField').then(m => ({ default: m.MultiSelectField })),
|
|
1923
|
+
'radio': () => import('./widgets/RadioField').then(m => ({ default: m.RadioField })),
|
|
1924
|
+
'checkboxes': () => import('./widgets/CheckboxesField').then(m => ({ default: m.CheckboxesField })),
|
|
1925
|
+
'tags': () => import('./widgets/TagsField').then(m => ({ default: m.TagsField })),
|
|
1926
|
+
|
|
1668
1927
|
// Specialized fields
|
|
1669
1928
|
'currency': () => import('./widgets/CurrencyField').then(m => ({ default: m.CurrencyField })),
|
|
1670
1929
|
'percent': () => import('./widgets/PercentField').then(m => ({ default: m.PercentField })),
|
|
1671
1930
|
'password': () => import('./widgets/PasswordField').then(m => ({ default: m.PasswordField })),
|
|
1672
1931
|
'markdown': () => import('./widgets/RichTextField').then(m => ({ default: m.RichTextField })),
|
|
1673
1932
|
'html': () => import('./widgets/RichTextField').then(m => ({ default: m.RichTextField })),
|
|
1933
|
+
'richtext': () => import('./widgets/RichTextField').then(m => ({ default: m.RichTextField })),
|
|
1674
1934
|
'lookup': () => import('./widgets/LookupField').then(m => ({ default: m.LookupField })),
|
|
1675
1935
|
// master_detail represents the child-side FK to its parent. In create/edit forms it
|
|
1676
1936
|
// must render as a single-value lookup picker (it is typically NOT NULL). The legacy
|
|
@@ -1719,6 +1979,33 @@ const fieldWidgetMap: Record<string, () => Promise<{ default: React.ComponentTyp
|
|
|
1719
1979
|
* // Register only the text field
|
|
1720
1980
|
* registerField('text');
|
|
1721
1981
|
*/
|
|
1982
|
+
// Field types whose short name collides with a display/ui/view/plugin component
|
|
1983
|
+
// registered elsewhere (e.g. the display widgets and form-input primitives in
|
|
1984
|
+
// @object-ui/components, or the markdown display plugin). These remain
|
|
1985
|
+
// accessible via the namespaced `field:<type>` key — which is how forms resolve
|
|
1986
|
+
// them (see form.tsx renderFieldComponent + mapFieldTypeToFormType) — but must
|
|
1987
|
+
// not overwrite the bare `<type>` fallback, which the display/ui primitive owns
|
|
1988
|
+
// and which page schemas expect (e.g. `{ type: 'markdown', content }` → the
|
|
1989
|
+
// markdown renderer, not the RichText editor). Without skipFallback each of
|
|
1990
|
+
// these logged a "bare-name fallback is being overwritten" warning at boot.
|
|
1991
|
+
const FIELD_TYPES_SKIP_FALLBACK = new Set([
|
|
1992
|
+
// Display widgets (text/html/image/avatar/grid live in @object-ui/components
|
|
1993
|
+
// or @object-ui/layout as the bare-name owners).
|
|
1994
|
+
'text',
|
|
1995
|
+
'html',
|
|
1996
|
+
'image',
|
|
1997
|
+
'avatar',
|
|
1998
|
+
'grid',
|
|
1999
|
+
// Form-input primitives owned by `ui:*` in @object-ui/components.
|
|
2000
|
+
'textarea',
|
|
2001
|
+
'select',
|
|
2002
|
+
'email',
|
|
2003
|
+
'password',
|
|
2004
|
+
'slider',
|
|
2005
|
+
// Display renderer owned by `plugin-markdown:markdown`.
|
|
2006
|
+
'markdown',
|
|
2007
|
+
]);
|
|
2008
|
+
|
|
1722
2009
|
export function registerField(fieldType: string): void {
|
|
1723
2010
|
const loader = fieldWidgetMap[fieldType];
|
|
1724
2011
|
if (!loader) {
|
|
@@ -1730,7 +2017,10 @@ export function registerField(fieldType: string): void {
|
|
|
1730
2017
|
const LazyFieldWidget = React.lazy(loader);
|
|
1731
2018
|
|
|
1732
2019
|
// Register with field namespace - NO WRAPPER to allow form renderer to control label/layout
|
|
1733
|
-
ComponentRegistry.register(fieldType, LazyFieldWidget, {
|
|
2020
|
+
ComponentRegistry.register(fieldType, LazyFieldWidget, {
|
|
2021
|
+
namespace: 'field',
|
|
2022
|
+
skipFallback: FIELD_TYPES_SKIP_FALLBACK.has(fieldType),
|
|
2023
|
+
});
|
|
1734
2024
|
}
|
|
1735
2025
|
|
|
1736
2026
|
/**
|
|
@@ -1755,26 +2045,43 @@ export function registerAllFields(): void {
|
|
|
1755
2045
|
*/
|
|
1756
2046
|
export function registerFields() {
|
|
1757
2047
|
// Basic fields - wrapped for documentation compatibility
|
|
1758
|
-
|
|
1759
|
-
|
|
2048
|
+
// `text` collides with the display text widget in @object-ui/components.
|
|
2049
|
+
// Display semantics ({ type: 'text', content: '...' }) are the dominant
|
|
2050
|
+
// usage across the docs/blocks catalog, so we keep this renderer accessible
|
|
2051
|
+
// only via the namespaced `field:text` key and let the display widget win
|
|
2052
|
+
// the bare `text` lookup.
|
|
2053
|
+
ComponentRegistry.register('text', createFieldRenderer(TextField), { namespace: 'field', skipFallback: true });
|
|
2054
|
+
// `textarea`/`select` collide with the `ui:*` form-input primitives that own
|
|
2055
|
+
// the bare keys; namespaced-only (forms use `field:<type>`). See the shared
|
|
2056
|
+
// FIELD_TYPES_SKIP_FALLBACK note above registerField().
|
|
2057
|
+
ComponentRegistry.register('textarea', createFieldRenderer(TextAreaField), { namespace: 'field', skipFallback: true });
|
|
1760
2058
|
ComponentRegistry.register('number', createFieldRenderer(NumberField), { namespace: 'field' });
|
|
1761
2059
|
ComponentRegistry.register('boolean', createFieldRenderer(BooleanField), { namespace: 'field' });
|
|
1762
|
-
ComponentRegistry.register('select', createFieldRenderer(SelectField), { namespace: 'field' });
|
|
2060
|
+
ComponentRegistry.register('select', createFieldRenderer(SelectField), { namespace: 'field', skipFallback: true });
|
|
1763
2061
|
ComponentRegistry.register('date', createFieldRenderer(DateField), { namespace: 'field' });
|
|
1764
2062
|
ComponentRegistry.register('datetime', createFieldRenderer(DateTimeField), { namespace: 'field' });
|
|
1765
2063
|
ComponentRegistry.register('time', createFieldRenderer(TimeField), { namespace: 'field' });
|
|
1766
2064
|
|
|
1767
2065
|
// Contact fields - wrapped for documentation compatibility
|
|
1768
|
-
|
|
2066
|
+
// `email` collides with the `ui:email` input variant; namespaced-only so the
|
|
2067
|
+
// display/input primitive keeps the bare key (forms use `field:email`).
|
|
2068
|
+
ComponentRegistry.register('email', createFieldRenderer(EmailField), { namespace: 'field', skipFallback: true });
|
|
1769
2069
|
ComponentRegistry.register('phone', createFieldRenderer(PhoneField), { namespace: 'field' });
|
|
1770
2070
|
ComponentRegistry.register('url', createFieldRenderer(UrlField), { namespace: 'field' });
|
|
1771
2071
|
|
|
1772
2072
|
// Specialized fields - wrapped for documentation compatibility
|
|
1773
2073
|
ComponentRegistry.register('currency', createFieldRenderer(CurrencyField), { namespace: 'field' });
|
|
1774
2074
|
ComponentRegistry.register('percent', createFieldRenderer(PercentField), { namespace: 'field' });
|
|
1775
|
-
|
|
1776
|
-
ComponentRegistry.register('
|
|
1777
|
-
|
|
2075
|
+
// `password` collides with the `ui:password` input variant; namespaced-only.
|
|
2076
|
+
ComponentRegistry.register('password', createFieldRenderer(PasswordField), { namespace: 'field', skipFallback: true });
|
|
2077
|
+
// `markdown` collides with the markdown DISPLAY renderer (plugin-markdown:markdown),
|
|
2078
|
+
// which owns bare `{ type: 'markdown', content }`; the field renderer here is the
|
|
2079
|
+
// RichText editor, reached by forms via `field:markdown`. Namespaced-only.
|
|
2080
|
+
ComponentRegistry.register('markdown', createFieldRenderer(RichTextField), { namespace: 'field', skipFallback: true });
|
|
2081
|
+
// `html` collides with the HTML-rendering display widget. Keep the
|
|
2082
|
+
// markdown field, but expose the HTML field only via `field:html` so the
|
|
2083
|
+
// display widget remains the default for { type: 'html', content }.
|
|
2084
|
+
ComponentRegistry.register('html', createFieldRenderer(RichTextField), { namespace: 'field', skipFallback: true });
|
|
1778
2085
|
ComponentRegistry.register('lookup', createFieldRenderer(LookupField), { namespace: 'field' });
|
|
1779
2086
|
// master_detail = child-side FK lookup (single value, typically required). See
|
|
1780
2087
|
// fieldWidgetMap above for rationale.
|
|
@@ -1782,7 +2089,8 @@ export function registerFields() {
|
|
|
1782
2089
|
|
|
1783
2090
|
// File fields
|
|
1784
2091
|
ComponentRegistry.register('file', createFieldRenderer(FileField), { namespace: 'field' });
|
|
1785
|
-
|
|
2092
|
+
// `image` collides with the display image widget; namespaced-only.
|
|
2093
|
+
ComponentRegistry.register('image', createFieldRenderer(ImageField), { namespace: 'field', skipFallback: true });
|
|
1786
2094
|
|
|
1787
2095
|
// Location field
|
|
1788
2096
|
ComponentRegistry.register('location', createFieldRenderer(LocationField), { namespace: 'field' });
|
|
@@ -1799,14 +2107,17 @@ export function registerFields() {
|
|
|
1799
2107
|
// Complex data types
|
|
1800
2108
|
ComponentRegistry.register('object', createFieldRenderer(ObjectField), { namespace: 'field' });
|
|
1801
2109
|
ComponentRegistry.register('vector', createFieldRenderer(VectorField), { namespace: 'field' });
|
|
1802
|
-
|
|
2110
|
+
// `grid` collides with the layout grid component; namespaced-only.
|
|
2111
|
+
ComponentRegistry.register('grid', createFieldRenderer(GridField), { namespace: 'field', skipFallback: true });
|
|
1803
2112
|
|
|
1804
2113
|
// NEW: Additional field types from @objectstack/spec
|
|
1805
2114
|
ComponentRegistry.register('color', createFieldRenderer(ColorField), { namespace: 'field' });
|
|
1806
|
-
|
|
2115
|
+
// `slider` collides with the `ui:slider` display control; namespaced-only.
|
|
2116
|
+
ComponentRegistry.register('slider', createFieldRenderer(SliderField), { namespace: 'field', skipFallback: true });
|
|
1807
2117
|
ComponentRegistry.register('rating', createFieldRenderer(RatingField), { namespace: 'field' });
|
|
1808
2118
|
ComponentRegistry.register('code', createFieldRenderer(CodeField), { namespace: 'field' });
|
|
1809
|
-
|
|
2119
|
+
// `avatar` collides with the display avatar widget; namespaced-only.
|
|
2120
|
+
ComponentRegistry.register('avatar', createFieldRenderer(AvatarField), { namespace: 'field', skipFallback: true });
|
|
1810
2121
|
ComponentRegistry.register('address', createFieldRenderer(AddressField), { namespace: 'field' });
|
|
1811
2122
|
ComponentRegistry.register('geolocation', createFieldRenderer(GeolocationField), { namespace: 'field' });
|
|
1812
2123
|
ComponentRegistry.register('signature', createFieldRenderer(SignatureField), { namespace: 'field' });
|
|
@@ -1854,6 +2165,10 @@ export * from './widgets/GeolocationField';
|
|
|
1854
2165
|
export * from './widgets/SignatureField';
|
|
1855
2166
|
export * from './widgets/QRCodeField';
|
|
1856
2167
|
export * from './widgets/MasterDetailField';
|
|
2168
|
+
export * from './widgets/MultiSelectField';
|
|
2169
|
+
export * from './widgets/RadioField';
|
|
2170
|
+
export * from './widgets/CheckboxesField';
|
|
2171
|
+
export * from './widgets/TagsField';
|
|
1857
2172
|
|
|
1858
2173
|
// Initialize registry
|
|
1859
2174
|
registerAllFields();
|