@object-ui/fields 6.2.3 → 7.1.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 (97) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/dist/complex-widgets.test.d.ts +1 -0
  3. package/dist/currency.d.ts +13 -0
  4. package/dist/datetime-widgets.test.d.ts +1 -0
  5. package/dist/field-type-coverage.test.d.ts +1 -0
  6. package/dist/index.cjs +4 -3
  7. package/dist/index.d.ts +300 -2
  8. package/dist/index.js +2460 -1274
  9. package/dist/standard-widgets.test.d.ts +0 -0
  10. package/dist/{packages/fields/src/widgets → widgets}/AddressField.d.ts +2 -1
  11. package/dist/{packages/fields/src/widgets → widgets}/AutoNumberField.d.ts +2 -1
  12. package/dist/{packages/fields/src/widgets → widgets}/AvatarField.d.ts +2 -1
  13. package/dist/{packages/fields/src/widgets → widgets}/BooleanField.d.ts +2 -1
  14. package/dist/widgets/CheckboxesField.d.ts +8 -0
  15. package/dist/{packages/fields/src/widgets → widgets}/CodeField.d.ts +2 -1
  16. package/dist/{packages/fields/src/widgets → widgets}/ColorField.d.ts +2 -1
  17. package/dist/{packages/fields/src/widgets → widgets}/CurrencyField.d.ts +2 -1
  18. package/dist/{packages/fields/src/widgets → widgets}/DateField.d.ts +2 -1
  19. package/dist/{packages/fields/src/widgets → widgets}/DateTimeField.d.ts +2 -1
  20. package/dist/{packages/fields/src/widgets → widgets}/EmailField.d.ts +2 -1
  21. package/dist/{packages/fields/src/widgets → widgets}/FileField.d.ts +2 -1
  22. package/dist/{packages/fields/src/widgets → widgets}/FormulaField.d.ts +2 -1
  23. package/dist/{packages/fields/src/widgets → widgets}/GeolocationField.d.ts +2 -1
  24. package/dist/widgets/GridField.d.ts +123 -0
  25. package/dist/widgets/GridField.test.d.ts +1 -0
  26. package/dist/{packages/fields/src/widgets → widgets}/ImageField.d.ts +2 -1
  27. package/dist/{packages/fields/src/widgets → widgets}/LocationField.d.ts +2 -1
  28. package/dist/{packages/fields/src/widgets → widgets}/LookupField.d.ts +2 -1
  29. package/dist/widgets/MarkdownContent.d.ts +9 -0
  30. package/dist/{packages/fields/src/widgets → widgets}/MasterDetailField.d.ts +2 -1
  31. package/dist/widgets/MultiSelectField.d.ts +9 -0
  32. package/dist/{packages/fields/src/widgets → widgets}/NumberField.d.ts +2 -1
  33. package/dist/{packages/fields/src/widgets → widgets}/ObjectField.d.ts +2 -1
  34. package/dist/{packages/fields/src/widgets → widgets}/PasswordField.d.ts +2 -1
  35. package/dist/{packages/fields/src/widgets → widgets}/PercentField.d.ts +2 -1
  36. package/dist/{packages/fields/src/widgets → widgets}/PhoneField.d.ts +2 -1
  37. package/dist/{packages/fields/src/widgets → widgets}/QRCodeField.d.ts +2 -1
  38. package/dist/widgets/RadioField.d.ts +8 -0
  39. package/dist/{packages/fields/src/widgets → widgets}/RatingField.d.ts +2 -1
  40. package/dist/{packages/fields/src/widgets → widgets}/RecordPickerDialog.d.ts +8 -2
  41. package/dist/{packages/fields/src/widgets → widgets}/RichTextField.d.ts +2 -1
  42. package/dist/{packages/fields/src/widgets → widgets}/SelectField.d.ts +2 -1
  43. package/dist/{packages/fields/src/widgets → widgets}/SignatureField.d.ts +2 -1
  44. package/dist/{packages/fields/src/widgets → widgets}/SliderField.d.ts +2 -1
  45. package/dist/{packages/fields/src/widgets → widgets}/SummaryField.d.ts +2 -1
  46. package/dist/widgets/TagsField.d.ts +8 -0
  47. package/dist/{packages/fields/src/widgets → widgets}/TextAreaField.d.ts +2 -1
  48. package/dist/{packages/fields/src/widgets → widgets}/TextField.d.ts +2 -1
  49. package/dist/{packages/fields/src/widgets → widgets}/TimeField.d.ts +2 -1
  50. package/dist/{packages/fields/src/widgets → widgets}/UrlField.d.ts +2 -1
  51. package/dist/{packages/fields/src/widgets → widgets}/UserField.d.ts +2 -1
  52. package/dist/{packages/fields/src/widgets → widgets}/VectorField.d.ts +2 -1
  53. package/dist/widgets/deriveLookupColumns.d.ts +36 -0
  54. package/dist/widgets/deriveLookupColumns.test.d.ts +1 -0
  55. package/dist/widgets/recentLookups.d.ts +13 -0
  56. package/dist/widgets/recentLookups.test.d.ts +1 -0
  57. package/dist/{packages/fields/src/widgets → widgets}/types.d.ts +1 -1
  58. package/package.json +14 -11
  59. package/src/__tests__/registry-collision.test.ts +91 -0
  60. package/src/__tests__/resolveFieldCurrency.test.ts +20 -0
  61. package/src/coerce-safe-value.test.ts +34 -0
  62. package/src/complex-widgets.test.tsx +58 -7
  63. package/src/currency.ts +14 -0
  64. package/src/field-type-coverage.test.ts +65 -0
  65. package/src/index.tsx +372 -57
  66. package/src/widgets/CheckboxesField.tsx +53 -0
  67. package/src/widgets/CurrencyField.tsx +5 -1
  68. package/src/widgets/GridField.test.tsx +309 -0
  69. package/src/widgets/GridField.tsx +907 -33
  70. package/src/widgets/LookupField.tsx +258 -107
  71. package/src/widgets/MarkdownContent.tsx +19 -0
  72. package/src/widgets/MultiSelectField.tsx +59 -0
  73. package/src/widgets/PercentField.tsx +19 -7
  74. package/src/widgets/RadioField.tsx +41 -0
  75. package/src/widgets/RecordPickerDialog.tsx +22 -18
  76. package/src/widgets/SelectField.tsx +23 -2
  77. package/src/widgets/TagsField.tsx +67 -0
  78. package/src/widgets/deriveLookupColumns.test.ts +74 -0
  79. package/src/widgets/deriveLookupColumns.ts +104 -0
  80. package/src/widgets/recentLookups.test.ts +45 -0
  81. package/src/widgets/recentLookups.ts +47 -0
  82. package/src/widgets/useFieldTranslation.ts +30 -0
  83. package/dist/packages/fields/src/fields-edge-cases.stories.d.ts +0 -45
  84. package/dist/packages/fields/src/fields.stories.d.ts +0 -56
  85. package/dist/packages/fields/src/index.d.ts +0 -286
  86. package/dist/packages/fields/src/record-picker-dialog.stories.d.ts +0 -25
  87. package/dist/packages/fields/src/widgets/GridField.d.ts +0 -7
  88. package/src/fields-edge-cases.stories.tsx +0 -407
  89. package/src/fields.stories.tsx +0 -566
  90. package/src/record-picker-dialog.stories.tsx +0 -160
  91. /package/dist/{packages/fields/src/__tests__/validation-feedback.test.d.ts → __tests__/registry-collision.test.d.ts} +0 -0
  92. /package/dist/{packages/fields/src/complex-widgets.test.d.ts → __tests__/resolveFieldCurrency.test.d.ts} +0 -0
  93. /package/dist/{packages/fields/src/standard-widgets.test.d.ts → __tests__/validation-feedback.test.d.ts} +0 -0
  94. /package/dist/{packages/fields/src/datetime-widgets.test.d.ts → coerce-safe-value.test.d.ts} +0 -0
  95. /package/dist/{packages/fields/src/widgets → widgets}/ImageCropperDialog.d.ts +0 -0
  96. /package/dist/{packages/fields/src/widgets → widgets}/_cell-renderer-bridge.d.ts +0 -0
  97. /package/dist/{packages/fields/src/widgets → widgets}/useFieldTranslation.d.ts +0 -0
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Recently-used lookup memory.
3
+ *
4
+ * Mainstream record pickers (Salesforce, Dataverse, SAP, ServiceNow) surface
5
+ * the user's recently-picked records the moment a lookup is focused — before
6
+ * any typing. We approximate that with a small per-object ring of the last few
7
+ * selected record ids, kept in localStorage (no backend dependency). When a
8
+ * recent-items API later exists, this module is the single seam to swap.
9
+ */
10
+ /** Most-recent-first list of record ids previously chosen for `objectName`. */
11
+ export declare function getRecentLookupIds(objectName: string): Array<string | number>;
12
+ /** Record that `id` was just chosen for `objectName` (moves it to the front). */
13
+ export declare function pushRecentLookupId(objectName: string, id: string | number | null | undefined): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { FieldMetadata } from '../../../types/src';
1
+ import { FieldMetadata } from '@object-ui/types';
2
2
  export type FieldWidgetProps<T = any> = {
3
3
  value: T;
4
4
  onChange: (val: T) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/fields",
3
- "version": "6.2.3",
3
+ "version": "7.1.0",
4
4
  "description": "Field renderers and registry for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,26 +25,29 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "clsx": "^2.1.1",
28
- "lucide-react": "^1.16.0",
28
+ "lucide-react": "^1.21.0",
29
+ "react-markdown": "^10.1.0",
30
+ "rehype-sanitize": "^6.0.0",
31
+ "remark-gfm": "^4.0.1",
29
32
  "tailwind-merge": "^3.6.0",
30
- "@object-ui/components": "6.2.3",
31
- "@object-ui/core": "6.2.3",
32
- "@object-ui/i18n": "6.2.3",
33
- "@object-ui/providers": "6.2.3",
34
- "@object-ui/react": "6.2.3",
35
- "@object-ui/types": "6.2.3"
33
+ "@object-ui/components": "7.1.0",
34
+ "@object-ui/core": "7.1.0",
35
+ "@object-ui/i18n": "7.1.0",
36
+ "@object-ui/providers": "7.1.0",
37
+ "@object-ui/react": "7.1.0",
38
+ "@object-ui/types": "7.1.0"
36
39
  },
37
40
  "peerDependencies": {
38
41
  "react": "^18.0.0 || ^19.0.0",
39
42
  "react-dom": "^18.0.0 || ^19.0.0"
40
43
  },
41
44
  "devDependencies": {
42
- "@types/react": "19.2.15",
45
+ "@types/react": "19.2.17",
43
46
  "@types/react-dom": "19.2.3",
44
47
  "@vitejs/plugin-react": "^6.0.2",
45
48
  "typescript": "^6.0.3",
46
- "vite": "^8.0.14",
47
- "vite-plugin-dts": "^5.0.1"
49
+ "vite": "^8.0.16",
50
+ "vite-plugin-dts": "^5.0.2"
48
51
  },
49
52
  "publishConfig": {
50
53
  "access": "public"
@@ -0,0 +1,91 @@
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
+ * Registry collision guard.
11
+ *
12
+ * `@object-ui/components` registers display widgets (`text`, `image`,
13
+ * `avatar`, `html`, `grid`) under their bare names. `@object-ui/fields`
14
+ * registers form-input widgets that — by name — collide with those.
15
+ *
16
+ * The fix: field registrations for these 5 types pass `skipFallback: true`,
17
+ * so the bare lookup keeps returning the display widget while the field
18
+ * input is still reachable via the `field:<type>` namespace.
19
+ *
20
+ * Regression contract:
21
+ * 1. `FIELD_TYPES_SKIP_FALLBACK` must contain every colliding name.
22
+ * 2. After `registerAllFields()`, bare lookups must NOT resolve to a
23
+ * field renderer for any of those names.
24
+ * 3. The `field:<type>` namespaced lookup must still work, so form
25
+ * schemas using `{ type: 'text' }` still find an input renderer
26
+ * when explicitly requested.
27
+ */
28
+
29
+ import { describe, it, expect, beforeAll } from 'vitest';
30
+ import { ComponentRegistry } from '@object-ui/core';
31
+
32
+ // Importing the fields entry runs `registerAllFields()` at module load.
33
+ // Importing the components entry runs display-widget registration the
34
+ // same way. Both must be loaded before we inspect the registry.
35
+ import '@object-ui/components';
36
+ import { registerAllFields } from '../index';
37
+
38
+ const COLLIDING_TYPES = ['text', 'html', 'image', 'avatar', 'grid'] as const;
39
+
40
+ describe('registry collision (fields ↔ components)', () => {
41
+ beforeAll(() => {
42
+ // Defensive: in case the auto-register at import time hasn't run yet
43
+ // under the test runner's module isolation.
44
+ registerAllFields();
45
+ });
46
+
47
+ it.each(COLLIDING_TYPES)(
48
+ 'bare "%s" lookup is NOT clobbered by the field renderer',
49
+ (type) => {
50
+ // The bare lookup must resolve, and it must resolve to a display widget
51
+ // (or a FallbackComponent / null) — never to the lazy field renderer.
52
+ // The field renderer is a React.lazy() result; the display widget is a
53
+ // plain function component. We don't pin to a specific identity, but we
54
+ // DO pin that whatever the bare lookup returns, the namespaced
55
+ // `field:<type>` lookup returns something different.
56
+ const bare = ComponentRegistry.get(type);
57
+ const namespaced = ComponentRegistry.get(`field:${type}`);
58
+
59
+ // Namespaced field renderer must exist for every colliding type —
60
+ // that's the whole point of keeping the field accessible.
61
+ expect(
62
+ namespaced,
63
+ `field:${type} must be registered so forms can still use it`,
64
+ ).toBeTruthy();
65
+
66
+ // The bare and namespaced renderers must NOT be identical. If they
67
+ // are, it means the field renderer wrote to the bare fallback and
68
+ // would render an empty <input> instead of the display widget.
69
+ expect(
70
+ bare,
71
+ `bare "${type}" must not be the same component as field:${type}`,
72
+ ).not.toBe(namespaced);
73
+ },
74
+ );
75
+
76
+ it('non-colliding field types DO populate the bare fallback', () => {
77
+ // Sanity check: a field type with no display-widget collision (e.g.
78
+ // `email`, `password`, `number`) must still register both at the
79
+ // namespaced AND bare keys, otherwise we'd silently break forms.
80
+ for (const type of ['email', 'password', 'number', 'date', 'select']) {
81
+ expect(
82
+ ComponentRegistry.get(`field:${type}`),
83
+ `field:${type} must be registered`,
84
+ ).toBeTruthy();
85
+ expect(
86
+ ComponentRegistry.get(type),
87
+ `bare "${type}" must also be reachable (no collision)`,
88
+ ).toBeTruthy();
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveFieldCurrency } from '../index';
3
+
4
+ describe('resolveFieldCurrency', () => {
5
+ it('prefers the field explicit currency over everything', () => {
6
+ expect(resolveFieldCurrency({ currency: 'JPY', currencyConfig: { defaultCurrency: 'EUR' } }, 'USD')).toBe('JPY');
7
+ });
8
+ it('falls back to currencyConfig.defaultCurrency, then legacy defaultCurrency', () => {
9
+ expect(resolveFieldCurrency({ currencyConfig: { defaultCurrency: 'EUR' } }, 'USD')).toBe('EUR');
10
+ expect(resolveFieldCurrency({ defaultCurrency: 'GBP' } as any, 'USD')).toBe('GBP');
11
+ });
12
+ it('falls back to the tenant default when the field omits its own', () => {
13
+ expect(resolveFieldCurrency({}, 'CNY')).toBe('CNY');
14
+ expect(resolveFieldCurrency(null, 'CNY')).toBe('CNY');
15
+ });
16
+ it('returns undefined when nothing is known (→ renderer shows a plain number)', () => {
17
+ expect(resolveFieldCurrency({})).toBeUndefined();
18
+ expect(resolveFieldCurrency(undefined, undefined)).toBeUndefined();
19
+ });
20
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { coerceToSafeValue } from './index';
3
+
4
+ describe('coerceToSafeValue — reference / lookup values', () => {
5
+ it('extracts a label from a JSON-string reference (unresolved external-id ref)', () => {
6
+ // Regression: a master_detail/lookup value can arrive as a JSON-encoded
7
+ // string; it must render a label, not raw '{"externalId":"..."}'.
8
+ expect(coerceToSafeValue('{"externalId":"Website Relaunch"}')).toBe('Website Relaunch');
9
+ });
10
+
11
+ it('extracts a label from a reference object, name > label > externalId > id', () => {
12
+ expect(coerceToSafeValue({ externalId: 'X' })).toBe('X');
13
+ expect(coerceToSafeValue({ name: 'N', externalId: 'X' })).toBe('N');
14
+ expect(coerceToSafeValue({ label: 'L', externalId: 'X' })).toBe('L');
15
+ expect(coerceToSafeValue({ id: 'id1' })).toBe('id1');
16
+ });
17
+
18
+ it('handles a JSON-string array of references', () => {
19
+ expect(coerceToSafeValue('[{"name":"A"},{"externalId":"B"}]')).toBe('A, B');
20
+ });
21
+
22
+ it('leaves plain strings and non-JSON-looking strings untouched', () => {
23
+ expect(coerceToSafeValue('Website Relaunch')).toBe('Website Relaunch');
24
+ expect(coerceToSafeValue('{not valid json')).toBe('{not valid json');
25
+ expect(coerceToSafeValue('hello')).toBe('hello');
26
+ });
27
+
28
+ it('passes through primitives and null/undefined', () => {
29
+ expect(coerceToSafeValue(42)).toBe(42);
30
+ expect(coerceToSafeValue(true)).toBe(true);
31
+ expect(coerceToSafeValue(null)).toBe(null);
32
+ expect(coerceToSafeValue(undefined)).toBe(undefined);
33
+ });
34
+ });
@@ -84,6 +84,7 @@ describe('Complex & Relationship Widgets', () => {
84
84
 
85
85
  beforeEach(() => {
86
86
  vi.clearAllMocks();
87
+ try { localStorage.clear(); } catch { /* jsdom */ }
87
88
  });
88
89
 
89
90
  it('fetches data from DataSource when dialog opens', async () => {
@@ -312,6 +313,53 @@ describe('Complex & Relationship Widgets', () => {
312
313
  expect(onCreateNew).toHaveBeenCalledWith('');
313
314
  });
314
315
 
316
+ it('quick-creates via dataSource.create when allow_create is set', async () => {
317
+ mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
318
+ mockDataSource.create.mockResolvedValue({ id: 'new-1', name: 'Acme' });
319
+ const onChange = vi.fn();
320
+ const field = { ...dynamicField, allow_create: true } as any;
321
+
322
+ render(<LookupField {...dynamicProps} field={field} onChange={onChange} />);
323
+
324
+ await act(async () => {
325
+ fireEvent.click(screen.getByRole('button', { name: /Select/i }));
326
+ });
327
+
328
+ const input = await screen.findByRole('combobox');
329
+ await act(async () => {
330
+ fireEvent.change(input, { target: { value: 'Acme' } });
331
+ });
332
+
333
+ const createBtn = await screen.findByText('Create new "Acme"');
334
+ await act(async () => {
335
+ fireEvent.click(createBtn);
336
+ });
337
+
338
+ await waitFor(() => {
339
+ expect(mockDataSource.create).toHaveBeenCalledWith('customers', { name: 'Acme' });
340
+ });
341
+ await waitFor(() => {
342
+ expect(onChange).toHaveBeenCalledWith('new-1');
343
+ });
344
+ });
345
+
346
+ it('shows a recently-used section on empty focus', async () => {
347
+ localStorage.setItem('objectui:lookup:recent:customers', JSON.stringify(['r1']));
348
+ mockDataSource.find.mockResolvedValue({ data: [{ id: 'a', name: 'Acme Corp' }], total: 1 });
349
+ mockDataSource.findOne.mockResolvedValue({ id: 'r1', name: 'Recent Co' });
350
+
351
+ render(<LookupField {...dynamicProps} />);
352
+
353
+ await act(async () => {
354
+ fireEvent.click(screen.getByRole('button', { name: /Select/i }));
355
+ });
356
+
357
+ await waitFor(() => {
358
+ expect(screen.getByText('Recently used')).toBeInTheDocument();
359
+ expect(screen.getByText('Recent Co')).toBeInTheDocument();
360
+ });
361
+ });
362
+
315
363
  it('navigates options with arrow keys and selects with Enter', async () => {
316
364
  const onChange = vi.fn();
317
365
  mockDataSource.find.mockResolvedValue({
@@ -522,8 +570,8 @@ describe('Complex & Relationship Widgets', () => {
522
570
 
523
571
  describe('GridField', () => {
524
572
  const columns = [
525
- { name: 'name', label: 'Name' },
526
- { name: 'age', label: 'Age' }
573
+ { field: 'name', label: 'Name' },
574
+ { field: 'age', label: 'Age', type: 'number' }
527
575
  ];
528
576
  const data = [
529
577
  { name: 'Alice', age: 30 },
@@ -534,17 +582,20 @@ describe('Complex & Relationship Widgets', () => {
534
582
  field: { ...mockField, columns }
535
583
  };
536
584
 
537
- it('renders row count in readonly', () => {
585
+ it('renders the rows as text in readonly mode', () => {
538
586
  render(<GridField {...gridProps} readonly value={data} />);
539
- expect(screen.getByText('2 rows')).toBeInTheDocument();
587
+ expect(screen.getByText('Alice')).toBeInTheDocument();
588
+ expect(screen.getByText('Bob')).toBeInTheDocument();
589
+ expect(screen.getByText('30')).toBeInTheDocument();
540
590
  });
541
591
 
542
- it('renders table with data in edit mode (readonly view)', () => {
592
+ it('renders an editable table with values in edit mode', () => {
543
593
  render(<GridField {...gridProps} value={data} />);
544
594
  expect(screen.getByRole('table')).toBeInTheDocument();
545
595
  expect(screen.getByText('Name')).toBeInTheDocument();
546
- expect(screen.getByText('Alice')).toBeInTheDocument();
547
- expect(screen.getByText('30')).toBeInTheDocument();
596
+ // edit mode renders inputs whose values are the row data
597
+ expect(screen.getByDisplayValue('Alice')).toBeInTheDocument();
598
+ expect(screen.getByDisplayValue('30')).toBeInTheDocument();
548
599
  });
549
600
  });
550
601
 
@@ -0,0 +1,14 @@
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';
@@ -0,0 +1,65 @@
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
+ 'formula', 'summary', 'auto_number',
31
+ ];
32
+
33
+ /**
34
+ * Field types that MUST resolve to a dedicated read/cell renderer — never the
35
+ * generic text cell renderer. (markdown/html/richtext render formatted;
36
+ * code/qrcode/time legitimately stay textual and are excluded.)
37
+ */
38
+ const CELL_RENDERER_TYPES = [
39
+ 'email', 'url', 'phone', 'number', 'currency', 'percent', 'progress',
40
+ 'boolean', 'toggle', 'date', 'datetime',
41
+ 'select', 'multiselect', 'radio', 'checkboxes', 'tags',
42
+ 'lookup', 'master_detail', 'tree',
43
+ 'file', 'video', 'audio', 'image', 'avatar', 'signature',
44
+ 'markdown', 'html', 'richtext',
45
+ 'location', 'geolocation', 'address', 'color', 'json',
46
+ 'formula', 'summary', 'user', 'owner',
47
+ ];
48
+
49
+ describe('field-type renderer coverage (regression guard)', () => {
50
+ it.each(FORM_WIDGET_TYPES)('maps "%s" to a dedicated form widget (not field:text)', (type) => {
51
+ const widget = mapFieldTypeToFormType(type);
52
+ expect(widget).toMatch(/^field:/);
53
+ expect(widget).not.toBe('field:text');
54
+ });
55
+
56
+ it.each(CELL_RENDERER_TYPES)('resolves "%s" to a dedicated cell renderer (not TextCellRenderer)', (type) => {
57
+ const renderer = getCellRenderer(type);
58
+ expect(renderer).toBeTypeOf('function');
59
+ expect(renderer).not.toBe(TextCellRenderer);
60
+ });
61
+
62
+ it('falls back to field:text for unknown types', () => {
63
+ expect(mapFieldTypeToFormType('something-unknown')).toBe('field:text');
64
+ });
65
+ });