@object-ui/plugin-detail 3.3.0 → 3.3.1

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 (134) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +21 -1
  3. package/dist/AddressField-LgHnO2Lk.js +98 -0
  4. package/dist/AutoNumberField-xZCrU0eW.js +14 -0
  5. package/dist/{AvatarField-Xuieq0ZI.js → AvatarField-Dy2XGlPz.js} +16 -15
  6. package/dist/{BooleanField-DwfMKknK.js → BooleanField-C0Clfka5.js} +11 -10
  7. package/dist/CodeField-CHUa07B6.js +23 -0
  8. package/dist/ColorField-vxHqEhcS.js +38 -0
  9. package/dist/CurrencyField-DiWjYWDo.js +49 -0
  10. package/dist/DateField-DGaRPM4P.js +22 -0
  11. package/dist/DateTimeField-8QnpsI_h.js +30 -0
  12. package/dist/EmailField-CkVgMbpI.js +26 -0
  13. package/dist/FileField-5UPV7uek.js +149 -0
  14. package/dist/FormulaField-BUgt6-Pi.js +17 -0
  15. package/dist/GeolocationField-D9T_jgG6.js +118 -0
  16. package/dist/GridField-DE_HwiIN.js +49 -0
  17. package/dist/ImageField-Dswnqtzf.js +73 -0
  18. package/dist/LocationField-gjqbE6na.js +36 -0
  19. package/dist/LookupField-BcS3LRKc.js +901 -0
  20. package/dist/{MasterDetailField-B0HTmmD7.js → MasterDetailField-BF6_-X3A.js} +20 -19
  21. package/dist/NumberField-Dj2rYmrS.js +27 -0
  22. package/dist/ObjectField-BymIojwd.js +50 -0
  23. package/dist/{PasswordField-DVTimsc3.js → PasswordField-ED_Xgqz-.js} +8 -7
  24. package/dist/PercentField-D-JKOxKC.js +61 -0
  25. package/dist/PhoneField-DSCaGYq7.js +26 -0
  26. package/dist/QRCodeField-CtcOUapi.js +73 -0
  27. package/dist/{RatingField-rRi_P0N0.js → RatingField-BDnyQFWy.js} +10 -9
  28. package/dist/RichTextField-CH6LVZQA.js +33 -0
  29. package/dist/SelectField-DE4dpkMV.js +36 -0
  30. package/dist/{SignatureField-2CnhcWI0.js → SignatureField-B1wh3f5A.js} +18 -17
  31. package/dist/{SliderField-DEpMVXko.js → SliderField-zoTCKh9n.js} +2 -1
  32. package/dist/SummaryField-BeBVT6VN.js +22 -0
  33. package/dist/TextAreaField-rfUGrRxh.js +37 -0
  34. package/dist/TextField-C_yM7ATQ.js +30 -0
  35. package/dist/TimeField-BcQmBZi9.js +22 -0
  36. package/dist/UrlField-BakaF6NI.js +31 -0
  37. package/dist/UserField-zS7y3eKb.js +76 -0
  38. package/dist/VectorField-CTZ4myDM.js +34 -0
  39. package/dist/index.js +1912 -1728
  40. package/dist/index.umd.cjs +38 -47
  41. package/dist/packages/plugin-detail/src/DetailSection.d.ts.map +1 -1
  42. package/dist/packages/plugin-detail/src/DetailView.d.ts +24 -0
  43. package/dist/packages/plugin-detail/src/DetailView.d.ts.map +1 -1
  44. package/dist/packages/plugin-detail/src/RelatedList.d.ts +8 -0
  45. package/dist/packages/plugin-detail/src/RelatedList.d.ts.map +1 -1
  46. package/dist/packages/plugin-detail/src/useDetailTranslation.d.ts.map +1 -1
  47. package/dist/plugin-detail.css +1 -2
  48. package/dist/rolldown-runtime-DnwLefa7.js +23 -0
  49. package/dist/{src-C56Ly5uG.js → src-DyUKLvMN.js} +18271 -26636
  50. package/dist/{useFieldTranslation-CkxqyB82.js → useFieldTranslation-BRgjC1oq.js} +1 -1
  51. package/package.json +33 -11
  52. package/.turbo/turbo-build.log +0 -64
  53. package/dist/AddressField-CDLSeyNx.js +0 -93
  54. package/dist/AutoNumberField-CtE7suf5.js +0 -14
  55. package/dist/CodeField-CfwgRxx2.js +0 -22
  56. package/dist/ColorField-YKHA7dBD.js +0 -37
  57. package/dist/CurrencyField-tvS3fPAF.js +0 -51
  58. package/dist/DateField-BKqXpkOh.js +0 -21
  59. package/dist/DateTimeField-CR-nJCE7.js +0 -32
  60. package/dist/EmailField-CgvW1Qal.js +0 -28
  61. package/dist/FileField-BVAme2ML.js +0 -151
  62. package/dist/FormulaField-DamJ2VaG.js +0 -14
  63. package/dist/GeolocationField-C99z7ZBM.js +0 -113
  64. package/dist/GridField-C9JbpTx_.js +0 -51
  65. package/dist/ImageField-CDANtgVV.js +0 -75
  66. package/dist/LocationField-ZSyZ0O-h.js +0 -35
  67. package/dist/LookupField-B3hQJt95.js +0 -903
  68. package/dist/LookupField-D00z6gn_.js +0 -2
  69. package/dist/NumberField-DL2QAL7X.js +0 -26
  70. package/dist/ObjectField-JYvUnuRO.js +0 -52
  71. package/dist/PercentField-DjR6BSpw.js +0 -63
  72. package/dist/PhoneField-CX1JL-jp.js +0 -28
  73. package/dist/QRCodeField-CH_1pU6R.js +0 -72
  74. package/dist/RichTextField-CJqLWlrb.js +0 -32
  75. package/dist/SelectField-DGoDoRM_.js +0 -30
  76. package/dist/SelectField-XBVI50AD.js +0 -2
  77. package/dist/SummaryField-7ch9aqAu.js +0 -19
  78. package/dist/TextAreaField-Cmw1oXcw.js +0 -36
  79. package/dist/TextField-OTLa3p51.js +0 -29
  80. package/dist/TimeField-DKPoNWoR.js +0 -21
  81. package/dist/UrlField-CxbmzP9f.js +0 -33
  82. package/dist/UserField-ChvwUkMK.js +0 -78
  83. package/dist/VectorField-BVClL8Vw.js +0 -36
  84. package/src/ActivityTimeline.tsx +0 -184
  85. package/src/CommentAttachment.tsx +0 -194
  86. package/src/CommentInput.tsx +0 -81
  87. package/src/DetailSection.tsx +0 -340
  88. package/src/DetailTabs.tsx +0 -73
  89. package/src/DetailView.stories.tsx +0 -334
  90. package/src/DetailView.tsx +0 -823
  91. package/src/DiffView.tsx +0 -233
  92. package/src/FieldChangeItem.tsx +0 -46
  93. package/src/HeaderHighlight.tsx +0 -88
  94. package/src/InlineCreateRelated.tsx +0 -291
  95. package/src/MentionAutocomplete.tsx +0 -123
  96. package/src/PointInTimeRestore.tsx +0 -261
  97. package/src/ReactionPicker.tsx +0 -106
  98. package/src/RecordActivityTimeline.tsx +0 -433
  99. package/src/RecordChatterPanel.tsx +0 -209
  100. package/src/RecordComments.tsx +0 -217
  101. package/src/RecordNavigationEnhanced.tsx +0 -213
  102. package/src/RelatedList.tsx +0 -413
  103. package/src/RelationshipGraph.tsx +0 -286
  104. package/src/RichTextCommentInput.tsx +0 -350
  105. package/src/SectionGroup.tsx +0 -101
  106. package/src/SubscriptionToggle.tsx +0 -62
  107. package/src/ThreadedReplies.tsx +0 -163
  108. package/src/__tests__/ActivityTimeline.test.tsx +0 -119
  109. package/src/__tests__/ActivityTimelineFiltering.test.tsx +0 -143
  110. package/src/__tests__/CommentInput.test.tsx +0 -57
  111. package/src/__tests__/DetailSection.test.tsx +0 -490
  112. package/src/__tests__/DetailView.test.tsx +0 -694
  113. package/src/__tests__/FieldChangeItem.test.tsx +0 -119
  114. package/src/__tests__/HeaderHighlight.test.tsx +0 -213
  115. package/src/__tests__/MentionAutocomplete.test.tsx +0 -97
  116. package/src/__tests__/ReactionPicker.test.tsx +0 -113
  117. package/src/__tests__/RecordActivityTimeline.test.tsx +0 -395
  118. package/src/__tests__/RecordChatterPanel.test.tsx +0 -265
  119. package/src/__tests__/RecordComments.test.tsx +0 -96
  120. package/src/__tests__/RecordCommentsPinSearch.test.tsx +0 -133
  121. package/src/__tests__/RelatedList.test.tsx +0 -160
  122. package/src/__tests__/SectionGroup.test.tsx +0 -101
  123. package/src/__tests__/SubscriptionToggle.test.tsx +0 -84
  124. package/src/__tests__/ThreadedReplies.test.tsx +0 -212
  125. package/src/__tests__/autoLayout.test.ts +0 -228
  126. package/src/__tests__/phase12-features.test.tsx +0 -583
  127. package/src/__tests__/roadmap-features.test.tsx +0 -478
  128. package/src/autoLayout.ts +0 -128
  129. package/src/index.tsx +0 -149
  130. package/src/useDetailTranslation.ts +0 -183
  131. package/tsconfig.json +0 -18
  132. package/vite.config.ts +0 -57
  133. package/vitest.config.ts +0 -13
  134. package/vitest.setup.ts +0 -1
@@ -1,478 +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
- import { describe, it, expect, vi } from 'vitest';
10
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
11
- import { DetailView } from '../DetailView';
12
- import { RelatedList } from '../RelatedList';
13
- import type { DetailViewSchema } from '@object-ui/types';
14
-
15
- describe('Roadmap Features', () => {
16
- // ── Feature 1: Auto-discover related lists ──
17
- describe('Auto-discover related lists', () => {
18
- it('should auto-discover related lists from objectSchema reference fields', async () => {
19
- const mockDataSource = {
20
- getObjectSchema: vi.fn().mockResolvedValue({
21
- fields: {
22
- name: { type: 'text' },
23
- account: { type: 'lookup', reference_to: 'account', label: 'Account' },
24
- contact: { type: 'master_detail', reference_to: 'contact', label: 'Primary Contact' },
25
- },
26
- }),
27
- findOne: vi.fn().mockResolvedValue({ name: 'Order 1' }),
28
- } as any;
29
-
30
- const schema: DetailViewSchema = {
31
- type: 'detail-view',
32
- title: 'Order Details',
33
- objectName: 'order',
34
- resourceId: 'order-1',
35
- fields: [{ name: 'name', label: 'Name' }],
36
- autoDiscoverRelated: true,
37
- };
38
-
39
- render(<DetailView schema={schema} dataSource={mockDataSource} />);
40
-
41
- // Wait for data to load
42
- await waitFor(() => {
43
- expect(screen.getByText('Order 1')).toBeInTheDocument();
44
- });
45
-
46
- // Should show auto-discovered related lists
47
- expect(screen.getByText('Account')).toBeInTheDocument();
48
- expect(screen.getByText('Primary Contact')).toBeInTheDocument();
49
- });
50
-
51
- it('should not auto-discover when autoDiscoverRelated is false', () => {
52
- const schema: DetailViewSchema = {
53
- type: 'detail-view',
54
- title: 'Order Details',
55
- data: { name: 'Order 1' },
56
- fields: [{ name: 'name', label: 'Name' }],
57
- autoDiscoverRelated: false,
58
- };
59
-
60
- render(<DetailView schema={schema} />);
61
- // Should not show "Related" heading
62
- expect(screen.queryByText('Related')).not.toBeInTheDocument();
63
- });
64
-
65
- it('should not auto-discover when explicit related lists are provided', async () => {
66
- const mockDataSource = {
67
- getObjectSchema: vi.fn().mockResolvedValue({
68
- fields: {
69
- name: { type: 'text' },
70
- account: { type: 'lookup', reference_to: 'account', label: 'Account' },
71
- },
72
- }),
73
- findOne: vi.fn().mockResolvedValue({ name: 'Order 1' }),
74
- } as any;
75
-
76
- const schema: DetailViewSchema = {
77
- type: 'detail-view',
78
- title: 'Order Details',
79
- objectName: 'order',
80
- resourceId: 'order-1',
81
- fields: [{ name: 'name', label: 'Name' }],
82
- autoDiscoverRelated: true,
83
- related: [
84
- { title: 'Custom Related', type: 'table', data: [] },
85
- ],
86
- };
87
-
88
- render(<DetailView schema={schema} dataSource={mockDataSource} />);
89
-
90
- await waitFor(() => {
91
- expect(screen.getByText('Order 1')).toBeInTheDocument();
92
- });
93
-
94
- // Should show explicit related, not auto-discovered
95
- expect(screen.getByText('Custom Related')).toBeInTheDocument();
96
- });
97
- });
98
-
99
- // ── Feature 2: Auto Tabs layout ──
100
- describe('Auto Tabs layout', () => {
101
- it('should render Details/Related/Activity tabs when autoTabs is true', () => {
102
- const schema: DetailViewSchema = {
103
- type: 'detail-view',
104
- title: 'Account Details',
105
- data: { name: 'Acme Corp' },
106
- fields: [{ name: 'name', label: 'Name' }],
107
- autoTabs: true,
108
- related: [
109
- { title: 'Contacts', type: 'table', data: [] },
110
- ],
111
- activities: [
112
- { id: '1', type: 'create', user: 'Bob', timestamp: '2026-02-15T10:00:00Z' },
113
- ],
114
- };
115
-
116
- render(<DetailView schema={schema} />);
117
-
118
- // All three tabs should be present
119
- expect(screen.getByText('Details')).toBeInTheDocument();
120
- expect(screen.getByText('Related')).toBeInTheDocument();
121
- });
122
-
123
- it('should show sections inside Details tab when autoTabs is true', () => {
124
- const schema: DetailViewSchema = {
125
- type: 'detail-view',
126
- title: 'Account Details',
127
- data: { name: 'Acme Corp', email: 'acme@example.com' },
128
- sections: [
129
- {
130
- title: 'Basic Info',
131
- fields: [
132
- { name: 'name', label: 'Name' },
133
- { name: 'email', label: 'Email' },
134
- ],
135
- },
136
- ],
137
- autoTabs: true,
138
- };
139
-
140
- render(<DetailView schema={schema} />);
141
-
142
- // Details tab should be active by default
143
- expect(screen.getByText('Basic Info')).toBeInTheDocument();
144
- expect(screen.getByText('Acme Corp')).toBeInTheDocument();
145
- });
146
-
147
- it('should not render autoTabs when explicit tabs are provided', () => {
148
- const schema: DetailViewSchema = {
149
- type: 'detail-view',
150
- title: 'Account',
151
- data: { name: 'Acme' },
152
- fields: [{ name: 'name', label: 'Name' }],
153
- autoTabs: true,
154
- tabs: [
155
- { key: 'custom', label: 'Custom Tab', content: { type: 'text', text: 'Custom' } },
156
- ],
157
- };
158
-
159
- render(<DetailView schema={schema} />);
160
- // Should not render auto-tabs Details/Related/Activity
161
- // Instead renders explicit tabs
162
- expect(screen.queryByRole('tab', { name: 'Details' })).not.toBeInTheDocument();
163
- });
164
- });
165
-
166
- // ── Feature 3: Related list row-level Edit/Delete ──
167
- describe('Related list row-level actions', () => {
168
- it('should render Edit button for each row when onRowEdit is provided', () => {
169
- const onRowEdit = vi.fn();
170
- const data = [
171
- { id: 1, name: 'Alice' },
172
- { id: 2, name: 'Bob' },
173
- ];
174
-
175
- render(
176
- <RelatedList
177
- title="Contacts"
178
- type="table"
179
- data={data}
180
- onRowEdit={onRowEdit}
181
- />
182
- );
183
-
184
- const editButtons = screen.getAllByText('Edit');
185
- expect(editButtons.length).toBe(2);
186
- });
187
-
188
- it('should call onRowEdit with the correct row when clicked', () => {
189
- const onRowEdit = vi.fn();
190
- const data = [{ id: 1, name: 'Alice' }];
191
-
192
- render(
193
- <RelatedList
194
- title="Contacts"
195
- type="table"
196
- data={data}
197
- onRowEdit={onRowEdit}
198
- />
199
- );
200
-
201
- fireEvent.click(screen.getByText('Edit'));
202
- expect(onRowEdit).toHaveBeenCalledWith({ id: 1, name: 'Alice' });
203
- });
204
-
205
- it('should render Delete button for each row when onRowDelete is provided', () => {
206
- const onRowDelete = vi.fn();
207
- const data = [
208
- { id: 1, name: 'Alice' },
209
- { id: 2, name: 'Bob' },
210
- ];
211
-
212
- render(
213
- <RelatedList
214
- title="Contacts"
215
- type="table"
216
- data={data}
217
- onRowDelete={onRowDelete}
218
- />
219
- );
220
-
221
- const deleteButtons = screen.getAllByText('Delete');
222
- expect(deleteButtons.length).toBe(2);
223
- });
224
-
225
- it('should call onRowDelete with the correct row after confirmation', () => {
226
- const onRowDelete = vi.fn();
227
- const data = [{ id: 1, name: 'Alice' }];
228
- const confirmSpy = vi.fn().mockReturnValue(true);
229
- window.confirm = confirmSpy;
230
-
231
- render(
232
- <RelatedList
233
- title="Contacts"
234
- type="table"
235
- data={data}
236
- onRowDelete={onRowDelete}
237
- />
238
- );
239
-
240
- fireEvent.click(screen.getByText('Delete'));
241
- expect(confirmSpy).toHaveBeenCalled();
242
- expect(onRowDelete).toHaveBeenCalledWith({ id: 1, name: 'Alice' });
243
- });
244
-
245
- it('should not call onRowDelete when confirmation is cancelled', () => {
246
- const onRowDelete = vi.fn();
247
- const data = [{ id: 1, name: 'Alice' }];
248
- const confirmSpy = vi.fn().mockReturnValue(false);
249
- window.confirm = confirmSpy;
250
-
251
- render(
252
- <RelatedList
253
- title="Contacts"
254
- type="table"
255
- data={data}
256
- onRowDelete={onRowDelete}
257
- />
258
- );
259
-
260
- fireEvent.click(screen.getByText('Delete'));
261
- expect(onRowDelete).not.toHaveBeenCalled();
262
- });
263
- });
264
-
265
- // ── Feature 4: Related list pagination, sorting, filtering ──
266
- describe('Related list pagination', () => {
267
- const manyItems = Array.from({ length: 15 }, (_, i) => ({
268
- id: i + 1,
269
- name: `Item ${i + 1}`,
270
- }));
271
-
272
- it('should show pagination controls when pageSize is set', () => {
273
- render(
274
- <RelatedList
275
- title="Items"
276
- type="table"
277
- data={manyItems}
278
- pageSize={5}
279
- />
280
- );
281
-
282
- expect(screen.getByText('Page 1 of 3')).toBeInTheDocument();
283
- expect(screen.getByText('Next')).toBeInTheDocument();
284
- expect(screen.getByText('Previous')).toBeInTheDocument();
285
- });
286
-
287
- it('should navigate to next page', () => {
288
- render(
289
- <RelatedList
290
- title="Items"
291
- type="table"
292
- data={manyItems}
293
- pageSize={5}
294
- />
295
- );
296
-
297
- fireEvent.click(screen.getByText('Next'));
298
- expect(screen.getByText('Page 2 of 3')).toBeInTheDocument();
299
- });
300
-
301
- it('should disable Previous on first page', () => {
302
- render(
303
- <RelatedList
304
- title="Items"
305
- type="table"
306
- data={manyItems}
307
- pageSize={5}
308
- />
309
- );
310
-
311
- const prevButton = screen.getByText('Previous').closest('button');
312
- expect(prevButton).toBeDisabled();
313
- });
314
-
315
- it('should not show pagination when all items fit on one page', () => {
316
- const fewItems = [{ id: 1, name: 'Item 1' }];
317
- render(
318
- <RelatedList
319
- title="Items"
320
- type="table"
321
- data={fewItems}
322
- pageSize={5}
323
- />
324
- );
325
-
326
- expect(screen.queryByText(/Page \d+ of \d+/)).not.toBeInTheDocument();
327
- });
328
- });
329
-
330
- describe('Related list filtering', () => {
331
- const data = [
332
- { id: 1, name: 'Alice' },
333
- { id: 2, name: 'Bob' },
334
- { id: 3, name: 'Charlie' },
335
- ];
336
-
337
- it('should render filter input when filterable is true', () => {
338
- render(
339
- <RelatedList
340
- title="Contacts"
341
- type="table"
342
- data={data}
343
- filterable={true}
344
- />
345
- );
346
-
347
- expect(screen.getByPlaceholderText('Filter...')).toBeInTheDocument();
348
- });
349
-
350
- it('should not render filter input when filterable is false', () => {
351
- render(
352
- <RelatedList
353
- title="Contacts"
354
- type="table"
355
- data={data}
356
- filterable={false}
357
- />
358
- );
359
-
360
- expect(screen.queryByPlaceholderText('Filter...')).not.toBeInTheDocument();
361
- });
362
- });
363
-
364
- describe('Related list sorting', () => {
365
- const data = [
366
- { id: 1, name: 'Charlie' },
367
- { id: 2, name: 'Alice' },
368
- { id: 3, name: 'Bob' },
369
- ];
370
-
371
- const columns = [
372
- { accessorKey: 'name', header: 'Name' },
373
- ];
374
-
375
- it('should render sort buttons when sortable is true', () => {
376
- render(
377
- <RelatedList
378
- title="Contacts"
379
- type="table"
380
- data={data}
381
- columns={columns}
382
- sortable={true}
383
- />
384
- );
385
-
386
- // Sort buttons include an ArrowUpDown icon
387
- const sortBtns = screen.getAllByRole('button').filter(btn =>
388
- btn.querySelector('.lucide-arrow-up-down')
389
- );
390
- expect(sortBtns.length).toBe(1);
391
- });
392
-
393
- it('should not render sort buttons when sortable is false', () => {
394
- render(
395
- <RelatedList
396
- title="Contacts"
397
- type="table"
398
- data={data}
399
- columns={columns}
400
- sortable={false}
401
- />
402
- );
403
-
404
- // Name appears as a sort button only when sortable; verify no ArrowUpDown icon
405
- const sortBtns = screen.queryAllByRole('button').filter(btn =>
406
- btn.querySelector('.lucide-arrow-up-down')
407
- );
408
- expect(sortBtns.length).toBe(0);
409
- });
410
- });
411
-
412
- // ── Feature 5: Collapsible section groups ──
413
- describe('Collapsible section groups in DetailView', () => {
414
- it('should render section groups', () => {
415
- const schema: DetailViewSchema = {
416
- type: 'detail-view',
417
- title: 'Account Details',
418
- data: { billingStreet: '123 Main St', shippingStreet: '456 Oak Ave' },
419
- fields: [],
420
- sectionGroups: [
421
- {
422
- title: 'Address Information',
423
- sections: [
424
- {
425
- title: 'Billing',
426
- fields: [{ name: 'billingStreet', label: 'Street' }],
427
- },
428
- {
429
- title: 'Shipping',
430
- fields: [{ name: 'shippingStreet', label: 'Street' }],
431
- },
432
- ],
433
- },
434
- ],
435
- };
436
-
437
- render(<DetailView schema={schema} />);
438
- expect(screen.getByText('Address Information')).toBeInTheDocument();
439
- expect(screen.getByText('Billing')).toBeInTheDocument();
440
- expect(screen.getByText('Shipping')).toBeInTheDocument();
441
- });
442
- });
443
-
444
- // ── Feature 6: Header highlight area ──
445
- describe('Header highlight area', () => {
446
- it('should render highlight fields below the header', () => {
447
- const schema: DetailViewSchema = {
448
- type: 'detail-view',
449
- title: 'Account Details',
450
- data: { name: 'Acme Corp', revenue: '$5M', employees: 150 },
451
- fields: [{ name: 'name', label: 'Name' }],
452
- highlightFields: [
453
- { name: 'revenue', label: 'Annual Revenue' },
454
- { name: 'employees', label: 'Employees' },
455
- ],
456
- };
457
-
458
- render(<DetailView schema={schema} />);
459
- expect(screen.getByText('Annual Revenue')).toBeInTheDocument();
460
- expect(screen.getByText('$5M')).toBeInTheDocument();
461
- expect(screen.getByText('Employees')).toBeInTheDocument();
462
- expect(screen.getByText('150')).toBeInTheDocument();
463
- });
464
-
465
- it('should not render highlight area when no highlightFields are provided', () => {
466
- const schema: DetailViewSchema = {
467
- type: 'detail-view',
468
- title: 'Account Details',
469
- data: { name: 'Acme Corp' },
470
- fields: [{ name: 'name', label: 'Name' }],
471
- };
472
-
473
- const { container } = render(<DetailView schema={schema} />);
474
- // No highlight card should be present
475
- expect(container.querySelector('.border-dashed')).not.toBeInTheDocument();
476
- });
477
- });
478
- });
package/src/autoLayout.ts DELETED
@@ -1,128 +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
- * Auto-Layout for DetailView
11
- *
12
- * Provides intelligent, zero-configuration default layout for detail sections.
13
- * When the user has not explicitly set columns on a section, this module
14
- * infers optimal column count based on the number of fields.
15
- *
16
- * Priority: User configuration > Auto-layout inference
17
- *
18
- * Column rules for detail views (wider thresholds than forms):
19
- * - 0-3 fields → 1 column
20
- * - 4-10 fields → 2 columns
21
- * - 11+ fields → 3 columns
22
- */
23
-
24
- import type { DetailViewField } from '@object-ui/types';
25
-
26
- /** Field types that should span full width in multi-column layouts */
27
- const WIDE_FIELD_TYPES = new Set([
28
- 'textarea',
29
- 'markdown',
30
- 'html',
31
- 'grid',
32
- 'rich-text',
33
- 'field:textarea',
34
- 'field:markdown',
35
- 'field:html',
36
- 'field:grid',
37
- 'field:rich-text',
38
- ]);
39
-
40
- /**
41
- * Check if a field type is "wide" (should span full row in multi-column layout).
42
- */
43
- export function isWideFieldType(type: string): boolean {
44
- return WIDE_FIELD_TYPES.has(type);
45
- }
46
-
47
- /**
48
- * Infer optimal number of columns for a detail section based on field count.
49
- * When containerWidth is provided, limits columns for narrower viewports.
50
- *
51
- * Rules (field-count based):
52
- * - 0-3 fields → 1 column
53
- * - 4-10 fields → 2 columns
54
- * - 11+ fields → 3 columns
55
- *
56
- * Responsive capping (when containerWidth is supplied):
57
- * - containerWidth < 640px → max 1 column
58
- * - containerWidth < 900px → max 2 columns
59
- * - containerWidth >= 900px → no cap
60
- */
61
- export function inferDetailColumns(fieldCount: number, containerWidth?: number): number {
62
- let cols: number;
63
- if (fieldCount <= 3) cols = 1;
64
- else if (fieldCount <= 10) cols = 2;
65
- else cols = 3;
66
-
67
- // Apply responsive capping when container width is known
68
- if (containerWidth !== undefined) {
69
- if (containerWidth < 640) return Math.min(cols, 1);
70
- if (containerWidth < 900) return Math.min(cols, 2);
71
- }
72
-
73
- return cols;
74
- }
75
-
76
- /**
77
- * Apply auto span to wide fields so they span the full row.
78
- * Only sets span if the field does not already have one explicitly set.
79
- *
80
- * @returns A new array of fields with span applied where needed.
81
- */
82
- export function applyAutoSpan(
83
- fields: DetailViewField[],
84
- columns: number
85
- ): DetailViewField[] {
86
- if (columns <= 1) return fields;
87
-
88
- return fields.map((field) => {
89
- // User-defined span takes priority
90
- if (field.span !== undefined) return field;
91
-
92
- // Wide field types should span full row
93
- if (field.type && isWideFieldType(field.type)) {
94
- return { ...field, span: columns };
95
- }
96
-
97
- return field;
98
- });
99
- }
100
-
101
- /**
102
- * Main auto-layout orchestrator for detail sections.
103
- * Applies intelligent defaults only when the user has not explicitly configured columns.
104
- *
105
- * @param fields - The section fields
106
- * @param schemaColumns - User-provided columns (from DetailViewSection or DetailViewSchema)
107
- * @param containerWidth - Optional container width in px for responsive column capping
108
- * @returns Object with processed fields and inferred columns
109
- */
110
- export function applyDetailAutoLayout(
111
- fields: DetailViewField[],
112
- schemaColumns: number | undefined,
113
- containerWidth?: number
114
- ): { fields: DetailViewField[]; columns: number } {
115
- // If user explicitly set columns, respect it but still apply auto span
116
- if (schemaColumns !== undefined) {
117
- const processed = applyAutoSpan(fields, schemaColumns);
118
- return { fields: processed, columns: schemaColumns };
119
- }
120
-
121
- // Infer columns from field count (with optional container-width capping)
122
- const columns = inferDetailColumns(fields.length, containerWidth);
123
-
124
- // Apply auto span for wide fields
125
- const processed = applyAutoSpan(fields, columns);
126
-
127
- return { fields: processed, columns };
128
- }