@object-ui/plugin-detail 3.0.3 → 3.1.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 (141) hide show
  1. package/.turbo/turbo-build.log +45 -8
  2. package/CHANGELOG.md +11 -0
  3. package/dist/AddressField-B1iVr404.js +96 -0
  4. package/dist/AutoNumberField-BxnFqllo.js +8 -0
  5. package/dist/AvatarField-Duw4xOLZ.js +82 -0
  6. package/dist/BooleanField-CZ4axVeq.js +37 -0
  7. package/dist/CodeField-BSz-mk2v.js +21 -0
  8. package/dist/ColorField-B522ad8m.js +42 -0
  9. package/dist/CurrencyField-Cwr3_pow.js +43 -0
  10. package/dist/DateField-DCo6dxud.js +21 -0
  11. package/dist/DateTimeField-BWfBuANO.js +28 -0
  12. package/dist/EmailField-CpwbdVCU.js +31 -0
  13. package/dist/FileField-DVAUAJ8e.js +133 -0
  14. package/dist/FormulaField-CJkkwIK8.js +9 -0
  15. package/dist/GeolocationField-DNCKitgo.js +123 -0
  16. package/dist/GridField-DSblZNfp.js +30 -0
  17. package/dist/ImageField-DBAlnMon.js +90 -0
  18. package/dist/LocationField-DsHsXA6R.js +31 -0
  19. package/dist/LookupField-CsT0QQz2.js +96 -0
  20. package/dist/MasterDetailField-Db8b7Gqs.js +108 -0
  21. package/dist/NumberField-0IGp7lcA.js +26 -0
  22. package/dist/ObjectField-BLApgJtS.js +48 -0
  23. package/dist/PasswordField-pHKyNlmo.js +38 -0
  24. package/dist/PercentField-CwgKmlIb.js +63 -0
  25. package/dist/PhoneField-lKtbYOdN.js +31 -0
  26. package/dist/QRCodeField-BTTasT3w.js +77 -0
  27. package/dist/RatingField-De2X-l44.js +47 -0
  28. package/dist/RichTextField-B5QnvUOr.js +38 -0
  29. package/dist/SelectField-C9AZRHWu.js +26 -0
  30. package/dist/SignatureField-BgcEmYzd.js +85 -0
  31. package/dist/SliderField-BzrttVOY.js +30 -0
  32. package/dist/SummaryField-ugYPYxjP.js +9 -0
  33. package/dist/TextAreaField-DSE_CaU6.js +39 -0
  34. package/dist/TextField-DFQ4T9PR.js +32 -0
  35. package/dist/TimeField-F0cfmsps.js +21 -0
  36. package/dist/UrlField-DLXrFIH-.js +33 -0
  37. package/dist/UserField-PXMmxJY9.js +49 -0
  38. package/dist/VectorField-CKg9jdGa.js +25 -0
  39. package/dist/index-qQ1C-yUR.js +59976 -0
  40. package/dist/index.js +32 -55026
  41. package/dist/index.umd.cjs +41 -30
  42. package/dist/plugin-detail.css +1 -1
  43. package/dist/src/ActivityTimeline.d.ts +20 -0
  44. package/dist/src/ActivityTimeline.d.ts.map +1 -0
  45. package/dist/src/CommentAttachment.d.ts +25 -0
  46. package/dist/src/CommentAttachment.d.ts.map +1 -0
  47. package/dist/src/CommentInput.d.ts +24 -0
  48. package/dist/src/CommentInput.d.ts.map +1 -0
  49. package/dist/src/DetailSection.d.ts +8 -0
  50. package/dist/src/DetailSection.d.ts.map +1 -1
  51. package/dist/src/DetailView.d.ts +4 -0
  52. package/dist/src/DetailView.d.ts.map +1 -1
  53. package/dist/src/DetailView.stories.d.ts +8 -0
  54. package/dist/src/DetailView.stories.d.ts.map +1 -1
  55. package/dist/src/DiffView.d.ts +24 -0
  56. package/dist/src/DiffView.d.ts.map +1 -0
  57. package/dist/src/FieldChangeItem.d.ts +21 -0
  58. package/dist/src/FieldChangeItem.d.ts.map +1 -0
  59. package/dist/src/HeaderHighlight.d.ts +18 -0
  60. package/dist/src/HeaderHighlight.d.ts.map +1 -0
  61. package/dist/src/InlineCreateRelated.d.ts +32 -0
  62. package/dist/src/InlineCreateRelated.d.ts.map +1 -0
  63. package/dist/src/MentionAutocomplete.d.ts +43 -0
  64. package/dist/src/MentionAutocomplete.d.ts.map +1 -0
  65. package/dist/src/PointInTimeRestore.d.ts +28 -0
  66. package/dist/src/PointInTimeRestore.d.ts.map +1 -0
  67. package/dist/src/ReactionPicker.d.ts +25 -0
  68. package/dist/src/ReactionPicker.d.ts.map +1 -0
  69. package/dist/src/RecordActivityTimeline.d.ts +49 -0
  70. package/dist/src/RecordActivityTimeline.d.ts.map +1 -0
  71. package/dist/src/RecordChatterPanel.d.ts +48 -0
  72. package/dist/src/RecordChatterPanel.d.ts.map +1 -0
  73. package/dist/src/RecordComments.d.ts +20 -0
  74. package/dist/src/RecordComments.d.ts.map +1 -0
  75. package/dist/src/RecordNavigationEnhanced.d.ts +18 -0
  76. package/dist/src/RecordNavigationEnhanced.d.ts.map +1 -0
  77. package/dist/src/RelatedList.d.ts +20 -0
  78. package/dist/src/RelatedList.d.ts.map +1 -1
  79. package/dist/src/RelationshipGraph.d.ts +23 -0
  80. package/dist/src/RelationshipGraph.d.ts.map +1 -0
  81. package/dist/src/RichTextCommentInput.d.ts +24 -0
  82. package/dist/src/RichTextCommentInput.d.ts.map +1 -0
  83. package/dist/src/SectionGroup.d.ts +21 -0
  84. package/dist/src/SectionGroup.d.ts.map +1 -0
  85. package/dist/src/SubscriptionToggle.d.ts +22 -0
  86. package/dist/src/SubscriptionToggle.d.ts.map +1 -0
  87. package/dist/src/ThreadedReplies.d.ts +26 -0
  88. package/dist/src/ThreadedReplies.d.ts.map +1 -0
  89. package/dist/src/autoLayout.d.ts +34 -0
  90. package/dist/src/autoLayout.d.ts.map +1 -0
  91. package/dist/src/index.d.ts +40 -0
  92. package/dist/src/index.d.ts.map +1 -1
  93. package/dist/src/useDetailTranslation.d.ts +34 -0
  94. package/dist/src/useDetailTranslation.d.ts.map +1 -0
  95. package/package.json +8 -7
  96. package/src/ActivityTimeline.tsx +184 -0
  97. package/src/CommentAttachment.tsx +192 -0
  98. package/src/CommentInput.tsx +81 -0
  99. package/src/DetailSection.tsx +81 -10
  100. package/src/DetailView.stories.tsx +76 -0
  101. package/src/DetailView.tsx +519 -66
  102. package/src/DiffView.tsx +231 -0
  103. package/src/FieldChangeItem.tsx +46 -0
  104. package/src/HeaderHighlight.tsx +67 -0
  105. package/src/InlineCreateRelated.tsx +291 -0
  106. package/src/MentionAutocomplete.tsx +123 -0
  107. package/src/PointInTimeRestore.tsx +261 -0
  108. package/src/ReactionPicker.tsx +106 -0
  109. package/src/RecordActivityTimeline.tsx +429 -0
  110. package/src/RecordChatterPanel.tsx +202 -0
  111. package/src/RecordComments.tsx +215 -0
  112. package/src/RecordNavigationEnhanced.tsx +211 -0
  113. package/src/RelatedList.tsx +314 -19
  114. package/src/RelationshipGraph.tsx +286 -0
  115. package/src/RichTextCommentInput.tsx +348 -0
  116. package/src/SectionGroup.tsx +101 -0
  117. package/src/SubscriptionToggle.tsx +60 -0
  118. package/src/ThreadedReplies.tsx +161 -0
  119. package/src/__tests__/ActivityTimeline.test.tsx +119 -0
  120. package/src/__tests__/ActivityTimelineFiltering.test.tsx +143 -0
  121. package/src/__tests__/CommentInput.test.tsx +57 -0
  122. package/src/__tests__/DetailSection.test.tsx +320 -0
  123. package/src/__tests__/DetailView.test.tsx +415 -1
  124. package/src/__tests__/FieldChangeItem.test.tsx +119 -0
  125. package/src/__tests__/HeaderHighlight.test.tsx +68 -0
  126. package/src/__tests__/MentionAutocomplete.test.tsx +97 -0
  127. package/src/__tests__/ReactionPicker.test.tsx +113 -0
  128. package/src/__tests__/RecordActivityTimeline.test.tsx +395 -0
  129. package/src/__tests__/RecordChatterPanel.test.tsx +227 -0
  130. package/src/__tests__/RecordComments.test.tsx +96 -0
  131. package/src/__tests__/RecordCommentsPinSearch.test.tsx +133 -0
  132. package/src/__tests__/RelatedList.test.tsx +160 -0
  133. package/src/__tests__/SectionGroup.test.tsx +101 -0
  134. package/src/__tests__/SubscriptionToggle.test.tsx +84 -0
  135. package/src/__tests__/ThreadedReplies.test.tsx +212 -0
  136. package/src/__tests__/autoLayout.test.ts +184 -0
  137. package/src/__tests__/phase12-features.test.tsx +583 -0
  138. package/src/__tests__/roadmap-features.test.tsx +478 -0
  139. package/src/autoLayout.ts +111 -0
  140. package/src/index.tsx +50 -0
  141. package/src/useDetailTranslation.ts +114 -0
@@ -0,0 +1,478 @@
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
+ });
@@ -0,0 +1,111 @@
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
+ *
50
+ * Rules:
51
+ * - 0-3 fields → 1 column
52
+ * - 4-10 fields → 2 columns
53
+ * - 11+ fields → 3 columns
54
+ */
55
+ export function inferDetailColumns(fieldCount: number): number {
56
+ if (fieldCount <= 3) return 1;
57
+ if (fieldCount <= 10) return 2;
58
+ return 3;
59
+ }
60
+
61
+ /**
62
+ * Apply auto span to wide fields so they span the full row.
63
+ * Only sets span if the field does not already have one explicitly set.
64
+ *
65
+ * @returns A new array of fields with span applied where needed.
66
+ */
67
+ export function applyAutoSpan(
68
+ fields: DetailViewField[],
69
+ columns: number
70
+ ): DetailViewField[] {
71
+ if (columns <= 1) return fields;
72
+
73
+ return fields.map((field) => {
74
+ // User-defined span takes priority
75
+ if (field.span !== undefined) return field;
76
+
77
+ // Wide field types should span full row
78
+ if (field.type && isWideFieldType(field.type)) {
79
+ return { ...field, span: columns };
80
+ }
81
+
82
+ return field;
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Main auto-layout orchestrator for detail sections.
88
+ * Applies intelligent defaults only when the user has not explicitly configured columns.
89
+ *
90
+ * @param fields - The section fields
91
+ * @param schemaColumns - User-provided columns (from DetailViewSection or DetailViewSchema)
92
+ * @returns Object with processed fields and inferred columns
93
+ */
94
+ export function applyDetailAutoLayout(
95
+ fields: DetailViewField[],
96
+ schemaColumns: number | undefined
97
+ ): { fields: DetailViewField[]; columns: number } {
98
+ // If user explicitly set columns, respect it but still apply auto span
99
+ if (schemaColumns !== undefined) {
100
+ const processed = applyAutoSpan(fields, schemaColumns);
101
+ return { fields: processed, columns: schemaColumns };
102
+ }
103
+
104
+ // Infer columns from field count
105
+ const columns = inferDetailColumns(fields.length);
106
+
107
+ // Apply auto span for wide fields
108
+ const processed = applyAutoSpan(fields, columns);
109
+
110
+ return { fields: processed, columns };
111
+ }
package/src/index.tsx CHANGED
@@ -14,10 +14,50 @@ import { RelatedList } from './RelatedList';
14
14
  import type { DetailViewSchema } from '@object-ui/types';
15
15
 
16
16
  export { DetailView, DetailSection, DetailTabs, RelatedList };
17
+ export { SectionGroup } from './SectionGroup';
18
+ export { HeaderHighlight } from './HeaderHighlight';
19
+ export { inferDetailColumns, isWideFieldType, applyAutoSpan, applyDetailAutoLayout } from './autoLayout';
20
+ export { useDetailTranslation, DETAIL_DEFAULT_TRANSLATIONS, createSafeTranslationHook } from './useDetailTranslation';
21
+ export { RecordComments } from './RecordComments';
22
+ export { ActivityTimeline } from './ActivityTimeline';
23
+ export { InlineCreateRelated } from './InlineCreateRelated';
24
+ export { RichTextCommentInput } from './RichTextCommentInput';
25
+ export { DiffView } from './DiffView';
26
+ export { RecordNavigationEnhanced } from './RecordNavigationEnhanced';
27
+ export { RelationshipGraph } from './RelationshipGraph';
28
+ export { CommentAttachment } from './CommentAttachment';
29
+ export { PointInTimeRestore } from './PointInTimeRestore';
30
+ export { RecordActivityTimeline } from './RecordActivityTimeline';
31
+ export { RecordChatterPanel } from './RecordChatterPanel';
32
+ export { CommentInput } from './CommentInput';
33
+ export { FieldChangeItem } from './FieldChangeItem';
34
+ export { MentionAutocomplete, createMentionFromSuggestion } from './MentionAutocomplete';
35
+ export { SubscriptionToggle } from './SubscriptionToggle';
36
+ export { ReactionPicker } from './ReactionPicker';
37
+ export { ThreadedReplies } from './ThreadedReplies';
17
38
  export type { DetailViewProps } from './DetailView';
18
39
  export type { DetailSectionProps } from './DetailSection';
19
40
  export type { DetailTabsProps } from './DetailTabs';
20
41
  export type { RelatedListProps } from './RelatedList';
42
+ export type { SectionGroupProps } from './SectionGroup';
43
+ export type { HeaderHighlightProps } from './HeaderHighlight';
44
+ export type { RecordCommentsProps } from './RecordComments';
45
+ export type { ActivityTimelineProps, ActivityFilterType } from './ActivityTimeline';
46
+ export type { InlineCreateRelatedProps, RelatedFieldDefinition, RelatedRecordOption } from './InlineCreateRelated';
47
+ export type { RichTextCommentInputProps, MentionSuggestion } from './RichTextCommentInput';
48
+ export type { DiffViewProps, DiffFieldType, DiffMode, DiffLine } from './DiffView';
49
+ export type { RecordNavigationEnhancedProps } from './RecordNavigationEnhanced';
50
+ export type { RelationshipGraphProps, GraphNode } from './RelationshipGraph';
51
+ export type { CommentAttachmentProps, Attachment } from './CommentAttachment';
52
+ export type { PointInTimeRestoreProps, RevisionEntry } from './PointInTimeRestore';
53
+ export type { RecordActivityTimelineProps, FeedFilterMode } from './RecordActivityTimeline';
54
+ export type { RecordChatterPanelProps } from './RecordChatterPanel';
55
+ export type { CommentInputProps } from './CommentInput';
56
+ export type { FieldChangeItemProps } from './FieldChangeItem';
57
+ export type { MentionAutocompleteProps, MentionSuggestionItem } from './MentionAutocomplete';
58
+ export type { SubscriptionToggleProps } from './SubscriptionToggle';
59
+ export type { ReactionPickerProps } from './ReactionPicker';
60
+ export type { ThreadedRepliesProps } from './ThreadedReplies';
21
61
 
22
62
  // Register DetailView component
23
63
  ComponentRegistry.register('detail-view', DetailView, {
@@ -31,14 +71,22 @@ ComponentRegistry.register('detail-view', DetailView, {
31
71
  { name: 'resourceId', type: 'string', label: 'Resource ID' },
32
72
  { name: 'api', type: 'string', label: 'API Endpoint' },
33
73
  { name: 'data', type: 'object', label: 'Data' },
74
+ { name: 'layout', type: 'enum', label: 'Layout Mode', enum: ['vertical', 'horizontal', 'grid'] },
75
+ { name: 'columns', type: 'number', label: 'Grid Columns' },
34
76
  { name: 'sections', type: 'array', label: 'Sections' },
35
77
  { name: 'fields', type: 'array', label: 'Fields' },
36
78
  { name: 'tabs', type: 'array', label: 'Tabs' },
37
79
  { name: 'related', type: 'array', label: 'Related Lists' },
38
80
  { name: 'actions', type: 'array', label: 'Actions' },
39
81
  { name: 'showBack', type: 'boolean', label: 'Show Back Button', defaultValue: true },
82
+ { name: 'backUrl', type: 'string', label: 'Back URL' },
40
83
  { name: 'showEdit', type: 'boolean', label: 'Show Edit Button', defaultValue: false },
84
+ { name: 'editUrl', type: 'string', label: 'Edit URL' },
41
85
  { name: 'showDelete', type: 'boolean', label: 'Show Delete Button', defaultValue: false },
86
+ { name: 'deleteConfirmation', type: 'string', label: 'Delete Confirmation Message' },
87
+ { name: 'loading', type: 'boolean', label: 'Show Loading State' },
88
+ { name: 'header', type: 'object', label: 'Custom Header' },
89
+ { name: 'footer', type: 'object', label: 'Custom Footer' },
42
90
  ],
43
91
  defaultProps: {
44
92
  title: 'Detail View',
@@ -64,6 +112,8 @@ ComponentRegistry.register('detail-section', DetailSection, {
64
112
  { name: 'collapsible', type: 'boolean', label: 'Collapsible', defaultValue: false },
65
113
  { name: 'defaultCollapsed', type: 'boolean', label: 'Default Collapsed', defaultValue: false },
66
114
  { name: 'columns', type: 'number', label: 'Columns', defaultValue: 2 },
115
+ { name: 'showBorder', type: 'boolean', label: 'Show Border', defaultValue: true },
116
+ { name: 'headerColor', type: 'string', label: 'Header Color' },
67
117
  ],
68
118
  });
69
119