@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,694 +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 type { DetailViewSchema } from '@object-ui/types';
13
-
14
- describe('DetailView', () => {
15
- it('should be exported', () => {
16
- expect(DetailView).toBeDefined();
17
- });
18
-
19
- it('should be a function', () => {
20
- expect(typeof DetailView).toBe('function');
21
- });
22
-
23
- it('should render with basic schema', () => {
24
- const schema: DetailViewSchema = {
25
- type: 'detail-view',
26
- title: 'Contact Details',
27
- data: {
28
- name: 'John Doe',
29
- email: 'john@example.com',
30
- },
31
- fields: [
32
- { name: 'name', label: 'Name' },
33
- { name: 'email', label: 'Email' },
34
- ],
35
- };
36
-
37
- const { container } = render(<DetailView schema={schema} />);
38
- expect(container).toBeTruthy();
39
- });
40
-
41
- it('should render title', () => {
42
- const schema: DetailViewSchema = {
43
- type: 'detail-view',
44
- title: 'Contact Details',
45
- data: { name: 'John Doe' },
46
- fields: [{ name: 'name', label: 'Name' }],
47
- };
48
-
49
- render(<DetailView schema={schema} />);
50
- expect(screen.getByText('Contact Details')).toBeInTheDocument();
51
- });
52
-
53
- it('should render back button when showBack is true', () => {
54
- const onBack = vi.fn();
55
- const schema: DetailViewSchema = {
56
- type: 'detail-view',
57
- title: 'Contact Details',
58
- data: { name: 'John Doe' },
59
- fields: [{ name: 'name', label: 'Name' }],
60
- showBack: true,
61
- };
62
-
63
- render(<DetailView schema={schema} onBack={onBack} />);
64
-
65
- const buttons = screen.getAllByRole('button');
66
- const backButton = buttons.find(btn =>
67
- btn.querySelector('svg') !== null
68
- );
69
-
70
- expect(backButton).toBeTruthy();
71
- });
72
-
73
- it('should call onBack when back button is clicked', () => {
74
- const onBack = vi.fn();
75
- const schema: DetailViewSchema = {
76
- type: 'detail-view',
77
- title: 'Contact Details',
78
- data: { name: 'John Doe' },
79
- fields: [{ name: 'name', label: 'Name' }],
80
- showBack: true,
81
- };
82
-
83
- render(<DetailView schema={schema} onBack={onBack} />);
84
-
85
- const buttons = screen.getAllByRole('button');
86
- const backButton = buttons.find(btn =>
87
- btn.querySelector('svg') !== null
88
- );
89
-
90
- if (backButton) {
91
- fireEvent.click(backButton);
92
- expect(onBack).toHaveBeenCalled();
93
- }
94
- });
95
-
96
- it('should render edit button when showEdit is true', () => {
97
- const schema: DetailViewSchema = {
98
- type: 'detail-view',
99
- title: 'Contact Details',
100
- data: { name: 'John Doe' },
101
- fields: [{ name: 'name', label: 'Name' }],
102
- showEdit: true,
103
- };
104
-
105
- render(<DetailView schema={schema} />);
106
-
107
- // Edit button should be present
108
- const buttons = screen.getAllByRole('button');
109
- expect(buttons.length).toBeGreaterThan(0);
110
- });
111
-
112
- it('should call onEdit when edit button is clicked', () => {
113
- const onEdit = vi.fn();
114
- const schema: DetailViewSchema = {
115
- type: 'detail-view',
116
- title: 'Contact Details',
117
- data: { name: 'John Doe' },
118
- fields: [{ name: 'name', label: 'Name' }],
119
- showEdit: true,
120
- // Disable back button to ensure it's not the first button found if using generic search
121
- showBack: false
122
- };
123
-
124
- render(<DetailView schema={schema} onEdit={onEdit} />);
125
-
126
- // Find button with text "Edit"
127
- const editButton = screen.getByRole('button', { name: /edit/i });
128
-
129
- if (editButton) {
130
- fireEvent.click(editButton);
131
- expect(onEdit).toHaveBeenCalled();
132
- }
133
- });
134
-
135
- it('should render delete button when showDelete is true', () => {
136
- const schema: DetailViewSchema = {
137
- type: 'detail-view',
138
- title: 'Contact Details',
139
- data: { name: 'John Doe' },
140
- fields: [{ name: 'name', label: 'Name' }],
141
- showDelete: true,
142
- };
143
-
144
- render(<DetailView schema={schema} />);
145
-
146
- const buttons = screen.getAllByRole('button');
147
- expect(buttons.length).toBeGreaterThan(0);
148
- });
149
-
150
- it('should render sections when provided', () => {
151
- const schema: DetailViewSchema = {
152
- type: 'detail-view',
153
- title: 'Contact Details',
154
- data: {
155
- name: 'John Doe',
156
- email: 'john@example.com',
157
- phone: '123-456-7890',
158
- },
159
- sections: [
160
- {
161
- title: 'Basic Information',
162
- fields: [
163
- { name: 'name', label: 'Name' },
164
- { name: 'email', label: 'Email' },
165
- ],
166
- },
167
- {
168
- title: 'Contact Information',
169
- fields: [
170
- { name: 'phone', label: 'Phone' },
171
- ],
172
- },
173
- ],
174
- };
175
-
176
- render(<DetailView schema={schema} />);
177
-
178
- expect(screen.getByText('Basic Information')).toBeInTheDocument();
179
- expect(screen.getByText('Contact Information')).toBeInTheDocument();
180
- });
181
-
182
- it('should render tabs when provided', () => {
183
- const schema: DetailViewSchema = {
184
- type: 'detail-view',
185
- title: 'Account Details',
186
- data: { name: 'Acme Corp' },
187
- tabs: [
188
- {
189
- key: 'details',
190
- label: 'Details',
191
- content: {
192
- type: 'text',
193
- text: 'Details content',
194
- },
195
- },
196
- {
197
- key: 'activity',
198
- label: 'Activity',
199
- content: {
200
- type: 'text',
201
- text: 'Activity content',
202
- },
203
- },
204
- ],
205
- };
206
-
207
- render(<DetailView schema={schema} />);
208
-
209
- expect(screen.getByText('Details')).toBeInTheDocument();
210
- expect(screen.getByText('Activity')).toBeInTheDocument();
211
- });
212
-
213
- it('should render related lists when provided', () => {
214
- const schema: DetailViewSchema = {
215
- type: 'detail-view',
216
- title: 'Account Details',
217
- data: { name: 'Acme Corp' },
218
- fields: [{ name: 'name', label: 'Name' }],
219
- related: [
220
- {
221
- title: 'Contacts',
222
- type: 'table',
223
- data: [],
224
- },
225
- ],
226
- };
227
-
228
- render(<DetailView schema={schema} />);
229
-
230
- expect(screen.getByText('Contacts')).toBeInTheDocument();
231
- });
232
-
233
- it('should show loading skeleton when loading is true', () => {
234
- const schema: DetailViewSchema = {
235
- type: 'detail-view',
236
- title: 'Contact Details',
237
- data: { name: 'John Doe' },
238
- fields: [{ name: 'name', label: 'Name' }],
239
- loading: true,
240
- };
241
-
242
- const { container } = render(<DetailView schema={schema} />);
243
-
244
- // Check for skeleton elements (they typically have animate-pulse class)
245
- // DetailedView uses Skeleton component which has animate-pulse class
246
- const skeletons = container.querySelectorAll('.animate-pulse');
247
- expect(skeletons.length).toBeGreaterThan(0);
248
- });
249
-
250
- it('should render prev/next navigation when recordNavigation is provided', () => {
251
- const onNavigate = vi.fn();
252
- const schema: DetailViewSchema = {
253
- type: 'detail-view',
254
- title: 'Contact Details',
255
- data: { name: 'John Doe' },
256
- fields: [{ name: 'name', label: 'Name' }],
257
- showBack: false,
258
- recordNavigation: {
259
- recordIds: ['id1', 'id2', 'id3'],
260
- currentIndex: 1,
261
- onNavigate,
262
- },
263
- };
264
-
265
- render(<DetailView schema={schema} />);
266
-
267
- // Should show position indicator
268
- expect(screen.getByText('2 of 3')).toBeInTheDocument();
269
- });
270
-
271
- it('should call onNavigate with previous record id', () => {
272
- const onNavigate = vi.fn();
273
- const schema: DetailViewSchema = {
274
- type: 'detail-view',
275
- title: 'Contact Details',
276
- data: { name: 'John Doe' },
277
- fields: [{ name: 'name', label: 'Name' }],
278
- showBack: false,
279
- recordNavigation: {
280
- recordIds: ['id1', 'id2', 'id3'],
281
- currentIndex: 1,
282
- onNavigate,
283
- },
284
- };
285
-
286
- const { container } = render(<DetailView schema={schema} />);
287
-
288
- // Find the prev button (first in the navigation group)
289
- const navButtons = container.querySelectorAll('button');
290
- // The prev button is the one that contains a chevron-left icon and is not disabled
291
- const prevButton = Array.from(navButtons).find(btn =>
292
- btn.querySelector('.lucide-chevron-left')
293
- );
294
- expect(prevButton).toBeTruthy();
295
- fireEvent.click(prevButton!);
296
- expect(onNavigate).toHaveBeenCalledWith('id1');
297
- });
298
-
299
- it('should call onNavigate with next record id', () => {
300
- const onNavigate = vi.fn();
301
- const schema: DetailViewSchema = {
302
- type: 'detail-view',
303
- title: 'Contact Details',
304
- data: { name: 'John Doe' },
305
- fields: [{ name: 'name', label: 'Name' }],
306
- showBack: false,
307
- recordNavigation: {
308
- recordIds: ['id1', 'id2', 'id3'],
309
- currentIndex: 1,
310
- onNavigate,
311
- },
312
- };
313
-
314
- const { container } = render(<DetailView schema={schema} />);
315
-
316
- const nextButton = Array.from(container.querySelectorAll('button')).find(btn =>
317
- btn.querySelector('.lucide-chevron-right')
318
- );
319
- expect(nextButton).toBeTruthy();
320
- fireEvent.click(nextButton!);
321
- expect(onNavigate).toHaveBeenCalledWith('id3');
322
- });
323
-
324
- it('should disable prev button at first record', () => {
325
- const schema: DetailViewSchema = {
326
- type: 'detail-view',
327
- title: 'Contact Details',
328
- data: { name: 'John Doe' },
329
- fields: [{ name: 'name', label: 'Name' }],
330
- showBack: false,
331
- recordNavigation: {
332
- recordIds: ['id1', 'id2'],
333
- currentIndex: 0,
334
- onNavigate: vi.fn(),
335
- },
336
- };
337
-
338
- const { container } = render(<DetailView schema={schema} />);
339
-
340
- const prevButton = Array.from(container.querySelectorAll('button')).find(btn =>
341
- btn.querySelector('.lucide-chevron-left')
342
- );
343
- expect(prevButton).toBeTruthy();
344
- expect(prevButton!).toBeDisabled();
345
- });
346
-
347
- it('should disable next button at last record', () => {
348
- const schema: DetailViewSchema = {
349
- type: 'detail-view',
350
- title: 'Contact Details',
351
- data: { name: 'John Doe' },
352
- fields: [{ name: 'name', label: 'Name' }],
353
- showBack: false,
354
- recordNavigation: {
355
- recordIds: ['id1', 'id2'],
356
- currentIndex: 1,
357
- onNavigate: vi.fn(),
358
- },
359
- };
360
-
361
- const { container } = render(<DetailView schema={schema} />);
362
-
363
- const nextButton = Array.from(container.querySelectorAll('button')).find(btn =>
364
- btn.querySelector('.lucide-chevron-right')
365
- );
366
- expect(nextButton).toBeTruthy();
367
- expect(nextButton!).toBeDisabled();
368
- });
369
-
370
- it('should render comments section when comments are provided', () => {
371
- const schema: DetailViewSchema = {
372
- type: 'detail-view',
373
- title: 'Contact Details',
374
- data: { name: 'John Doe' },
375
- fields: [{ name: 'name', label: 'Name' }],
376
- comments: [
377
- {
378
- id: '1',
379
- text: 'Great contact!',
380
- author: 'Alice',
381
- createdAt: '2026-02-16T08:00:00Z',
382
- },
383
- ],
384
- };
385
-
386
- render(<DetailView schema={schema} />);
387
-
388
- expect(screen.getByText('Comments')).toBeInTheDocument();
389
- expect(screen.getByText('Great contact!')).toBeInTheDocument();
390
- expect(screen.getByText('Alice')).toBeInTheDocument();
391
- });
392
-
393
- it('should render activity timeline when activities are provided', () => {
394
- const schema: DetailViewSchema = {
395
- type: 'detail-view',
396
- title: 'Contact Details',
397
- data: { name: 'John Doe' },
398
- fields: [{ name: 'name', label: 'Name' }],
399
- activities: [
400
- {
401
- id: '1',
402
- type: 'create',
403
- user: 'Bob',
404
- timestamp: '2026-02-15T10:00:00Z',
405
- },
406
- {
407
- id: '2',
408
- type: 'field_change',
409
- field: 'email',
410
- oldValue: 'old@test.com',
411
- newValue: 'new@test.com',
412
- user: 'Alice',
413
- timestamp: '2026-02-16T09:00:00Z',
414
- },
415
- ],
416
- };
417
-
418
- render(<DetailView schema={schema} />);
419
-
420
- expect(screen.getByText('Activity')).toBeInTheDocument();
421
- expect(screen.getByText('Bob')).toBeInTheDocument();
422
- });
423
-
424
- it('should render primaryField value as header title', () => {
425
- const schema: DetailViewSchema = {
426
- type: 'detail-view',
427
- title: 'Contact',
428
- primaryField: 'name',
429
- data: { name: 'John Doe', email: 'john@example.com' },
430
- fields: [
431
- { name: 'name', label: 'Name' },
432
- { name: 'email', label: 'Email' },
433
- ],
434
- };
435
-
436
- render(<DetailView schema={schema} />);
437
- // The h1 heading should show the primary field value
438
- const heading = screen.getByRole('heading', { level: 1 });
439
- expect(heading.textContent).toBe('John Doe');
440
- });
441
-
442
- it('should fall back to title when primaryField value is empty', () => {
443
- const schema: DetailViewSchema = {
444
- type: 'detail-view',
445
- title: 'Contact',
446
- primaryField: 'name',
447
- data: { email: 'john@example.com' },
448
- fields: [
449
- { name: 'name', label: 'Name' },
450
- { name: 'email', label: 'Email' },
451
- ],
452
- };
453
-
454
- render(<DetailView schema={schema} />);
455
- const heading = screen.getByRole('heading', { level: 1 });
456
- expect(heading.textContent).toBe('Contact');
457
- });
458
-
459
- it('should render summaryFields as badges', () => {
460
- const schema: DetailViewSchema = {
461
- type: 'detail-view',
462
- title: 'Contact',
463
- primaryField: 'name',
464
- summaryFields: ['status', 'department'],
465
- data: { name: 'Jane Doe', status: 'Active', department: 'Engineering' },
466
- fields: [
467
- { name: 'name', label: 'Name' },
468
- { name: 'status', label: 'Status' },
469
- { name: 'department', label: 'Department' },
470
- ],
471
- };
472
-
473
- render(<DetailView schema={schema} />);
474
- const heading = screen.getByRole('heading', { level: 1 });
475
- expect(heading.textContent).toBe('Jane Doe');
476
- // Summary badges should be present (they appear both as badges and as field values)
477
- const activeElements = screen.getAllByText('Active');
478
- expect(activeElements.length).toBeGreaterThanOrEqual(1);
479
- const engElements = screen.getAllByText('Engineering');
480
- expect(engElements.length).toBeGreaterThanOrEqual(1);
481
- });
482
-
483
- it('should not render summary badge for empty values', () => {
484
- const schema: DetailViewSchema = {
485
- type: 'detail-view',
486
- title: 'Contact',
487
- summaryFields: ['status', 'department'],
488
- data: { name: 'Jane Doe', status: 'Active', department: null },
489
- fields: [
490
- { name: 'name', label: 'Name' },
491
- { name: 'status', label: 'Status' },
492
- ],
493
- };
494
-
495
- const { container } = render(<DetailView schema={schema} />);
496
- // The header area should have a badge for 'Active' but not 'department'
497
- // Find badges within the header
498
- const headerBadges = container.querySelectorAll('.border-b .rounded-full');
499
- const badgeTexts = Array.from(headerBadges).map(b => b.textContent);
500
- expect(badgeTexts).toContain('Active');
501
- });
502
-
503
- it('should show "Record not found" when data is null after loading', async () => {
504
- const mockDataSource = {
505
- findOne: vi.fn().mockResolvedValue(null),
506
- } as any;
507
-
508
- const schema: DetailViewSchema = {
509
- type: 'detail-view',
510
- title: 'Contact Details',
511
- objectName: 'contact',
512
- resourceId: 'nonexistent-id',
513
- fields: [{ name: 'name', label: 'Name' }],
514
- };
515
-
516
- const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} />);
517
- expect(await findByText('Record not found')).toBeInTheDocument();
518
- expect(await findByText(/does not exist or may have been deleted/)).toBeInTheDocument();
519
- });
520
-
521
- it('should show "Go back" button in "Record not found" state when showBack is true', async () => {
522
- const mockDataSource = {
523
- findOne: vi.fn().mockResolvedValue(null),
524
- } as any;
525
- const onBack = vi.fn();
526
-
527
- const schema: DetailViewSchema = {
528
- type: 'detail-view',
529
- title: 'Contact Details',
530
- objectName: 'contact',
531
- resourceId: 'nonexistent-id',
532
- fields: [{ name: 'name', label: 'Name' }],
533
- showBack: true,
534
- };
535
-
536
- const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} onBack={onBack} />);
537
- const goBackBtn = await findByText('Go back');
538
- fireEvent.click(goBackBtn);
539
- expect(onBack).toHaveBeenCalled();
540
- });
541
-
542
- it('should try fallback with alternate ID when first findOne throws an error', async () => {
543
- let callCount = 0;
544
- const mockDataSource = {
545
- findOne: vi.fn().mockImplementation((_obj: string, id: string) => {
546
- callCount++;
547
- if (callCount === 1) {
548
- // First call throws (simulate server error)
549
- return Promise.reject(new Error('Server error'));
550
- }
551
- // Second call (fallback) succeeds
552
- return Promise.resolve({ name: 'Alice' });
553
- }),
554
- } as any;
555
-
556
- const schema: DetailViewSchema = {
557
- type: 'detail-view',
558
- title: 'Contact Details',
559
- objectName: 'contact',
560
- resourceId: 'contact-123',
561
- fields: [{ name: 'name', label: 'Name' }],
562
- };
563
-
564
- const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} />);
565
- // The fallback should find the record using the stripped ID
566
- expect(await findByText('Alice')).toBeInTheDocument();
567
- // findOne should be called twice: first with original ID, then with stripped prefix
568
- expect(mockDataSource.findOne).toHaveBeenCalledTimes(2);
569
- expect(mockDataSource.findOne).toHaveBeenNthCalledWith(1, 'contact', 'contact-123');
570
- expect(mockDataSource.findOne).toHaveBeenNthCalledWith(2, 'contact', '123');
571
- });
572
-
573
- it('should call findOne with $expand when objectSchema has lookup fields', async () => {
574
- const mockDataSource = {
575
- getObjectSchema: vi.fn().mockResolvedValue({
576
- fields: {
577
- name: { type: 'text' },
578
- customer: { type: 'lookup', reference_to: 'contact' },
579
- account: { type: 'master_detail', reference_to: 'account' },
580
- },
581
- }),
582
- findOne: vi.fn().mockResolvedValue({ name: 'Order 1', customer: { name: 'Alice' }, account: { name: 'Acme' } }),
583
- } as any;
584
-
585
- const schema: DetailViewSchema = {
586
- type: 'detail-view',
587
- title: 'Order Details',
588
- objectName: 'order',
589
- resourceId: 'order-1',
590
- fields: [
591
- { name: 'name', label: 'Name' },
592
- { name: 'customer', label: 'Customer' },
593
- { name: 'account', label: 'Account' },
594
- ],
595
- };
596
-
597
- render(<DetailView schema={schema} dataSource={mockDataSource} />);
598
-
599
- await waitFor(() => {
600
- expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('order');
601
- expect(mockDataSource.findOne).toHaveBeenCalledWith(
602
- 'order',
603
- 'order-1',
604
- expect.objectContaining({ $expand: expect.arrayContaining(['customer', 'account']) }),
605
- );
606
- });
607
- });
608
-
609
- it('should call findOne without $expand when objectSchema has no lookup fields', async () => {
610
- const mockDataSource = {
611
- getObjectSchema: vi.fn().mockResolvedValue({
612
- fields: {
613
- name: { type: 'text' },
614
- email: { type: 'text' },
615
- },
616
- }),
617
- findOne: vi.fn().mockResolvedValue({ name: 'Alice', email: 'alice@example.com' }),
618
- } as any;
619
-
620
- const schema: DetailViewSchema = {
621
- type: 'detail-view',
622
- title: 'Contact Details',
623
- objectName: 'contact',
624
- resourceId: 'c1',
625
- fields: [
626
- { name: 'name', label: 'Name' },
627
- { name: 'email', label: 'Email' },
628
- ],
629
- };
630
-
631
- render(<DetailView schema={schema} dataSource={mockDataSource} />);
632
-
633
- await waitFor(() => {
634
- // When no lookup fields exist, findOne should be called without $expand params
635
- expect(mockDataSource.findOne).toHaveBeenCalledWith('contact', 'c1');
636
- });
637
- });
638
-
639
- it('should still work when getObjectSchema is not available on dataSource', async () => {
640
- const mockDataSource = {
641
- findOne: vi.fn().mockResolvedValue({ name: 'Bob' }),
642
- } as any;
643
-
644
- const schema: DetailViewSchema = {
645
- type: 'detail-view',
646
- title: 'Contact Details',
647
- objectName: 'contact',
648
- resourceId: 'c1',
649
- fields: [{ name: 'name', label: 'Name' }],
650
- };
651
-
652
- const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} />);
653
- expect(await findByText('Bob')).toBeInTheDocument();
654
- });
655
-
656
- it('should use i18n fallback for "Record not found" text', async () => {
657
- const mockDataSource = {
658
- findOne: vi.fn().mockResolvedValue(null),
659
- } as any;
660
-
661
- const schema: DetailViewSchema = {
662
- type: 'detail-view',
663
- title: 'Contact Details',
664
- objectName: 'contact',
665
- resourceId: 'nonexistent-id',
666
- fields: [{ name: 'name', label: 'Name' }],
667
- };
668
-
669
- const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} />);
670
- // These use the default English translations from useDetailTranslation fallback
671
- expect(await findByText('Record not found')).toBeInTheDocument();
672
- expect(await findByText('Go back')).toBeInTheDocument();
673
- });
674
-
675
- it('should use i18n fallback for related section heading', () => {
676
- const schema: DetailViewSchema = {
677
- type: 'detail-view',
678
- title: 'Account Details',
679
- data: { name: 'Acme Corp' },
680
- fields: [{ name: 'name', label: 'Name' }],
681
- related: [
682
- {
683
- title: 'Contacts',
684
- type: 'table',
685
- data: [],
686
- },
687
- ],
688
- };
689
-
690
- render(<DetailView schema={schema} />);
691
- // The "Related" heading uses t('detail.related')
692
- expect(screen.getByText('Related')).toBeInTheDocument();
693
- });
694
- });