@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,395 +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 } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import { RecordActivityTimeline } from '../RecordActivityTimeline';
13
- import type { FeedItem } from '@object-ui/types';
14
-
15
- const mockItems: FeedItem[] = [
16
- {
17
- id: '1',
18
- type: 'comment',
19
- actor: 'Alice',
20
- body: 'This looks great!',
21
- createdAt: '2026-02-20T10:00:00Z',
22
- },
23
- {
24
- id: '2',
25
- type: 'field_change',
26
- actor: 'Bob',
27
- createdAt: '2026-02-20T11:00:00Z',
28
- fieldChanges: [
29
- {
30
- field: 'status',
31
- fieldLabel: 'Status',
32
- oldDisplayValue: 'Open',
33
- newDisplayValue: 'In Progress',
34
- },
35
- ],
36
- },
37
- {
38
- id: '3',
39
- type: 'task',
40
- actor: 'Charlie',
41
- body: 'Follow up with client',
42
- createdAt: '2026-02-20T12:00:00Z',
43
- },
44
- {
45
- id: '4',
46
- type: 'system',
47
- actor: 'System',
48
- body: 'Record created automatically',
49
- createdAt: '2026-02-20T08:00:00Z',
50
- },
51
- ];
52
-
53
- const allTypeItems: FeedItem[] = [
54
- { id: 'c1', type: 'comment', actor: 'A', body: 'comment', createdAt: '2026-02-20T10:00:00Z' },
55
- { id: 'fc1', type: 'field_change', actor: 'B', createdAt: '2026-02-20T10:01:00Z', fieldChanges: [{ field: 'x', oldValue: '1', newValue: '2' }] },
56
- { id: 't1', type: 'task', actor: 'C', body: 'task', createdAt: '2026-02-20T10:02:00Z' },
57
- { id: 'e1', type: 'event', actor: 'D', body: 'event', createdAt: '2026-02-20T10:03:00Z' },
58
- { id: 's1', type: 'system', actor: 'E', body: 'system', createdAt: '2026-02-20T10:04:00Z' },
59
- { id: 'm1', type: 'email', actor: 'F', body: 'email', createdAt: '2026-02-20T10:05:00Z' },
60
- { id: 'p1', type: 'call', actor: 'G', body: 'call', createdAt: '2026-02-20T10:06:00Z' },
61
- ];
62
-
63
- describe('RecordActivityTimeline', () => {
64
- it('should render activity heading with count', () => {
65
- render(<RecordActivityTimeline items={mockItems} />);
66
- expect(screen.getByText('Activity')).toBeInTheDocument();
67
- expect(screen.getByText('(4)')).toBeInTheDocument();
68
- });
69
-
70
- it('should render actor names', () => {
71
- render(<RecordActivityTimeline items={mockItems} />);
72
- expect(screen.getByText('Alice')).toBeInTheDocument();
73
- expect(screen.getByText('Bob')).toBeInTheDocument();
74
- expect(screen.getByText('Charlie')).toBeInTheDocument();
75
- expect(screen.getByText('System')).toBeInTheDocument();
76
- });
77
-
78
- it('should render comment body text', () => {
79
- render(<RecordActivityTimeline items={mockItems} />);
80
- expect(screen.getByText('This looks great!')).toBeInTheDocument();
81
- });
82
-
83
- it('should render field change entries', () => {
84
- render(<RecordActivityTimeline items={mockItems} />);
85
- expect(screen.getByText('Status')).toBeInTheDocument();
86
- expect(screen.getByText('Open')).toBeInTheDocument();
87
- expect(screen.getByText('In Progress')).toBeInTheDocument();
88
- });
89
-
90
- it('should show "No activity recorded" when empty', () => {
91
- render(<RecordActivityTimeline items={[]} />);
92
- expect(screen.getByText('No activity recorded')).toBeInTheDocument();
93
- });
94
-
95
- it('should not show "No activity recorded" when collapseWhenEmpty is true', () => {
96
- render(<RecordActivityTimeline items={[]} collapseWhenEmpty />);
97
- expect(screen.queryByText('No activity recorded')).not.toBeInTheDocument();
98
- });
99
-
100
- it('should filter to comments only', () => {
101
- render(
102
- <RecordActivityTimeline items={mockItems} filterMode="comments_only" />,
103
- );
104
- expect(screen.getByText('(1)')).toBeInTheDocument();
105
- expect(screen.getByText('Alice')).toBeInTheDocument();
106
- expect(screen.queryByText('Bob')).not.toBeInTheDocument();
107
- });
108
-
109
- it('should filter to changes only', () => {
110
- render(
111
- <RecordActivityTimeline items={mockItems} filterMode="changes_only" />,
112
- );
113
- expect(screen.getByText('(1)')).toBeInTheDocument();
114
- expect(screen.getByText('Bob')).toBeInTheDocument();
115
- expect(screen.queryByText('Alice')).not.toBeInTheDocument();
116
- });
117
-
118
- it('should filter to tasks only', () => {
119
- render(
120
- <RecordActivityTimeline items={mockItems} filterMode="tasks_only" />,
121
- );
122
- expect(screen.getByText('(1)')).toBeInTheDocument();
123
- expect(screen.getByText('Charlie')).toBeInTheDocument();
124
- });
125
-
126
- it('should show filter dropdown by default', () => {
127
- render(<RecordActivityTimeline items={mockItems} />);
128
- expect(screen.getByLabelText('Filter activity')).toBeInTheDocument();
129
- });
130
-
131
- it('should hide filter when showFilterToggle is false', () => {
132
- render(
133
- <RecordActivityTimeline
134
- items={mockItems}
135
- config={{ showFilterToggle: false }}
136
- />,
137
- );
138
- expect(screen.queryByLabelText('Filter activity')).not.toBeInTheDocument();
139
- });
140
-
141
- it('should show comment input when onAddComment provided', () => {
142
- const onAdd = vi.fn();
143
- render(
144
- <RecordActivityTimeline items={[]} onAddComment={onAdd} />,
145
- );
146
- expect(screen.getByPlaceholderText(/Leave a comment/)).toBeInTheDocument();
147
- });
148
-
149
- it('should hide comment input when showCommentInput is false', () => {
150
- const onAdd = vi.fn();
151
- render(
152
- <RecordActivityTimeline
153
- items={[]}
154
- onAddComment={onAdd}
155
- config={{ showCommentInput: false }}
156
- />,
157
- );
158
- expect(screen.queryByPlaceholderText(/Leave a comment/)).not.toBeInTheDocument();
159
- });
160
-
161
- it('should call onAddComment when comment is submitted', () => {
162
- const onAdd = vi.fn().mockResolvedValue(undefined);
163
- render(
164
- <RecordActivityTimeline items={[]} onAddComment={onAdd} />,
165
- );
166
- fireEvent.change(screen.getByPlaceholderText(/Leave a comment/), {
167
- target: { value: 'New comment' },
168
- });
169
- fireEvent.click(screen.getByLabelText('Submit (Ctrl+Enter)'));
170
- expect(onAdd).toHaveBeenCalledWith('New comment');
171
- });
172
-
173
- it('should show Load more button when hasMore is true', () => {
174
- render(
175
- <RecordActivityTimeline items={mockItems} hasMore onLoadMore={() => {}} />,
176
- );
177
- expect(screen.getByLabelText('Load more')).toBeInTheDocument();
178
- });
179
-
180
- it('should call onLoadMore when Load more is clicked', () => {
181
- const onLoadMore = vi.fn().mockResolvedValue(undefined);
182
- render(
183
- <RecordActivityTimeline items={mockItems} hasMore onLoadMore={onLoadMore} />,
184
- );
185
- fireEvent.click(screen.getByLabelText('Load more'));
186
- expect(onLoadMore).toHaveBeenCalled();
187
- });
188
-
189
- it('should render source label when present', () => {
190
- const items: FeedItem[] = [
191
- { id: '1', type: 'comment', actor: 'Alice', body: 'Hi', createdAt: '2026-02-20T10:00:00Z', source: 'email' },
192
- ];
193
- render(<RecordActivityTimeline items={items} />);
194
- expect(screen.getByText('via email')).toBeInTheDocument();
195
- });
196
-
197
- it('should render edited indicator', () => {
198
- const items: FeedItem[] = [
199
- { id: '1', type: 'comment', actor: 'Alice', body: 'Edited', createdAt: '2026-02-20T10:00:00Z', edited: true },
200
- ];
201
- render(<RecordActivityTimeline items={items} />);
202
- expect(screen.getByText('(edited)')).toBeInTheDocument();
203
- });
204
-
205
- it('should render pinned indicator', () => {
206
- const items: FeedItem[] = [
207
- { id: '1', type: 'comment', actor: 'Alice', body: 'Pinned comment', createdAt: '2026-02-20T10:00:00Z', pinned: true },
208
- ];
209
- render(<RecordActivityTimeline items={items} />);
210
- expect(screen.getByText('📌 Pinned')).toBeInTheDocument();
211
- });
212
-
213
- it('should show subscription toggle when configured', () => {
214
- render(
215
- <RecordActivityTimeline
216
- items={[]}
217
- config={{ showSubscriptionToggle: true }}
218
- subscription={{ recordId: '1', subscribed: true }}
219
- onToggleSubscription={() => {}}
220
- />,
221
- );
222
- expect(screen.getByLabelText('Unsubscribe from notifications')).toBeInTheDocument();
223
- });
224
-
225
- it('should render all 7 item types', () => {
226
- render(<RecordActivityTimeline items={allTypeItems} />);
227
- expect(screen.getByText('A')).toBeInTheDocument();
228
- expect(screen.getByText('B')).toBeInTheDocument();
229
- expect(screen.getByText('C')).toBeInTheDocument();
230
- expect(screen.getByText('D')).toBeInTheDocument();
231
- expect(screen.getByText('E')).toBeInTheDocument();
232
- expect(screen.getByText('F')).toBeInTheDocument();
233
- expect(screen.getByText('G')).toBeInTheDocument();
234
- });
235
-
236
- it('should render actor avatar when actorAvatarUrl is provided', () => {
237
- const items: FeedItem[] = [
238
- { id: '1', type: 'comment', actor: 'Alice', actorAvatarUrl: 'https://example.com/alice.png', body: 'Hi', createdAt: '2026-02-20T10:00:00Z' },
239
- ];
240
- render(<RecordActivityTimeline items={items} />);
241
- const img = screen.getByAltText('Alice');
242
- expect(img).toBeInTheDocument();
243
- expect(img).toHaveAttribute('src', 'https://example.com/alice.png');
244
- });
245
-
246
- it('should submit comment via Ctrl+Enter', () => {
247
- const onAdd = vi.fn().mockResolvedValue(undefined);
248
- render(
249
- <RecordActivityTimeline items={[]} onAddComment={onAdd} />,
250
- );
251
- const textarea = screen.getByPlaceholderText(/Leave a comment/);
252
- fireEvent.change(textarea, { target: { value: 'Ctrl enter test' } });
253
- fireEvent.keyDown(textarea, { key: 'Enter', ctrlKey: true });
254
- expect(onAdd).toHaveBeenCalledWith('Ctrl enter test');
255
- });
256
-
257
- it('should clear input after successful comment submission', async () => {
258
- const onAdd = vi.fn().mockResolvedValue(undefined);
259
- render(
260
- <RecordActivityTimeline items={[]} onAddComment={onAdd} />,
261
- );
262
- const textarea = screen.getByPlaceholderText(/Leave a comment/) as HTMLTextAreaElement;
263
- fireEvent.change(textarea, { target: { value: 'Will be cleared' } });
264
- fireEvent.click(screen.getByLabelText('Submit (Ctrl+Enter)'));
265
- await vi.waitFor(() => {
266
- expect(textarea.value).toBe('');
267
- });
268
- });
269
-
270
- it('should disable input and button during submission', async () => {
271
- let resolveSubmit: () => void;
272
- const onAdd = vi.fn(() => new Promise<void>((r) => { resolveSubmit = r; }));
273
- render(
274
- <RecordActivityTimeline items={[]} onAddComment={onAdd} />,
275
- );
276
- const textarea = screen.getByPlaceholderText(/Leave a comment/) as HTMLTextAreaElement;
277
- fireEvent.change(textarea, { target: { value: 'submitting' } });
278
- fireEvent.click(screen.getByLabelText('Submit (Ctrl+Enter)'));
279
- expect(textarea).toBeDisabled();
280
- expect(screen.getByLabelText('Submit (Ctrl+Enter)')).toBeDisabled();
281
- resolveSubmit!();
282
- await vi.waitFor(() => {
283
- expect(textarea).not.toBeDisabled();
284
- });
285
- });
286
-
287
- it('should show loading spinner when loading more', async () => {
288
- let resolveLoad: () => void;
289
- const onLoadMore = vi.fn(() => new Promise<void>((r) => { resolveLoad = r; }));
290
- render(
291
- <RecordActivityTimeline items={mockItems} hasMore onLoadMore={onLoadMore} />,
292
- );
293
- fireEvent.click(screen.getByLabelText('Load more'));
294
- // Loader2 spinner should be present while loading
295
- expect(screen.getByLabelText('Load more')).toBeDisabled();
296
- resolveLoad!();
297
- await vi.waitFor(() => {
298
- expect(screen.getByLabelText('Load more')).not.toBeDisabled();
299
- });
300
- });
301
-
302
- it('should use controlled filterMode and call onFilterChange', () => {
303
- const onFilterChange = vi.fn();
304
- render(
305
- <RecordActivityTimeline
306
- items={mockItems}
307
- filterMode="comments_only"
308
- onFilterChange={onFilterChange}
309
- />,
310
- );
311
- expect(screen.getByText('Alice')).toBeInTheDocument();
312
- expect(screen.queryByText('Bob')).not.toBeInTheDocument();
313
- // Change filter via select
314
- fireEvent.change(screen.getByLabelText('Filter activity'), { target: { value: 'all' } });
315
- expect(onFilterChange).toHaveBeenCalledWith('all');
316
- });
317
-
318
- it('should use internal filter state when no controlled filterMode', () => {
319
- render(
320
- <RecordActivityTimeline items={mockItems} />,
321
- );
322
- // Initially all visible
323
- expect(screen.getByText('Alice')).toBeInTheDocument();
324
- expect(screen.getByText('Bob')).toBeInTheDocument();
325
- // Switch to comments_only
326
- fireEvent.change(screen.getByLabelText('Filter activity'), { target: { value: 'comments_only' } });
327
- expect(screen.getByText('Alice')).toBeInTheDocument();
328
- expect(screen.queryByText('Bob')).not.toBeInTheDocument();
329
- });
330
-
331
- it('should not render comment input when onAddComment is not provided', () => {
332
- render(<RecordActivityTimeline items={[]} />);
333
- expect(screen.queryByPlaceholderText(/Leave a comment/)).not.toBeInTheDocument();
334
- });
335
-
336
- it('should render reactions when enableReactions is true', () => {
337
- const items: FeedItem[] = [
338
- {
339
- id: '1',
340
- type: 'comment',
341
- actor: 'Alice',
342
- body: 'Nice!',
343
- createdAt: '2026-02-20T10:00:00Z',
344
- reactions: [{ emoji: '👍', count: 2, reacted: true }],
345
- },
346
- ];
347
- render(
348
- <RecordActivityTimeline items={items} config={{ enableReactions: true }} />,
349
- );
350
- expect(screen.getByText('👍')).toBeInTheDocument();
351
- expect(screen.getByText('2')).toBeInTheDocument();
352
- });
353
-
354
- it('should not render reactions when enableReactions is false', () => {
355
- const items: FeedItem[] = [
356
- {
357
- id: '1',
358
- type: 'comment',
359
- actor: 'Alice',
360
- body: 'Nice!',
361
- createdAt: '2026-02-20T10:00:00Z',
362
- reactions: [{ emoji: '👍', count: 2, reacted: true }],
363
- },
364
- ];
365
- render(
366
- <RecordActivityTimeline items={items} config={{ enableReactions: false }} />,
367
- );
368
- expect(screen.queryByText('👍')).not.toBeInTheDocument();
369
- });
370
-
371
- it('should group items by parentId when enableThreading is true', () => {
372
- const items: FeedItem[] = [
373
- { id: 'p1', type: 'comment', actor: 'Alice', body: 'Root comment', createdAt: '2026-02-20T10:00:00Z', replyCount: 1 },
374
- { id: 'r1', type: 'comment', actor: 'Bob', body: 'Reply', createdAt: '2026-02-20T11:00:00Z', parentId: 'p1' },
375
- ];
376
- render(
377
- <RecordActivityTimeline items={items} config={{ enableThreading: true }} />,
378
- );
379
- // Root comment rendered
380
- expect(screen.getByText('Alice')).toBeInTheDocument();
381
- expect(screen.getByText('Root comment')).toBeInTheDocument();
382
- // Reply is shown as threaded reply (collapsed)
383
- expect(screen.getByText('1 reply')).toBeInTheDocument();
384
- // Reply actor should not be directly visible (collapsed in ThreadedReplies)
385
- expect(screen.queryByText('Bob')).not.toBeInTheDocument();
386
- });
387
-
388
- it('should show "via {source}" label', () => {
389
- const items: FeedItem[] = [
390
- { id: '1', type: 'comment', actor: 'Alice', body: 'Hi', createdAt: '2026-02-20T10:00:00Z', source: 'slack' },
391
- ];
392
- render(<RecordActivityTimeline items={items} />);
393
- expect(screen.getByText('via slack')).toBeInTheDocument();
394
- });
395
- });
@@ -1,265 +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 } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import { RecordChatterPanel } from '../RecordChatterPanel';
13
- import type { FeedItem } from '@object-ui/types';
14
-
15
- const mockItems: FeedItem[] = [
16
- {
17
- id: '1',
18
- type: 'comment',
19
- actor: 'Alice',
20
- body: 'Hello from chatter',
21
- createdAt: '2026-02-20T10:00:00Z',
22
- },
23
- {
24
- id: '2',
25
- type: 'field_change',
26
- actor: 'Bob',
27
- createdAt: '2026-02-20T11:00:00Z',
28
- fieldChanges: [
29
- { field: 'priority', fieldLabel: 'Priority', oldValue: 'low', newValue: 'high' },
30
- ],
31
- },
32
- ];
33
-
34
- describe('RecordChatterPanel', () => {
35
- describe('sidebar mode (right)', () => {
36
- it('should render Discussion header in sidebar mode', () => {
37
- render(
38
- <RecordChatterPanel config={{ position: 'right' }} items={mockItems} />,
39
- );
40
- expect(screen.getByText('Discussion')).toBeInTheDocument();
41
- });
42
-
43
- it('should render activity items', () => {
44
- render(
45
- <RecordChatterPanel config={{ position: 'right' }} items={mockItems} />,
46
- );
47
- expect(screen.getByText('Alice')).toBeInTheDocument();
48
- expect(screen.getByText('Hello from chatter')).toBeInTheDocument();
49
- });
50
-
51
- it('should collapse when close button is clicked', () => {
52
- render(
53
- <RecordChatterPanel
54
- config={{ position: 'right', collapsible: true }}
55
- items={mockItems}
56
- />,
57
- );
58
- // Initially expanded (not defaultCollapsed)
59
- expect(screen.getByText('Discussion')).toBeInTheDocument();
60
- fireEvent.click(screen.getByLabelText('Close discussion panel'));
61
- // Now collapsed — show expand button
62
- expect(screen.getByLabelText('Open discussion panel')).toBeInTheDocument();
63
- });
64
-
65
- it('should start collapsed when defaultCollapsed is true', () => {
66
- render(
67
- <RecordChatterPanel
68
- config={{ position: 'right', collapsible: true, defaultCollapsed: true }}
69
- items={mockItems}
70
- />,
71
- );
72
- expect(screen.getByLabelText('Open discussion panel')).toBeInTheDocument();
73
- expect(screen.queryByText('Discussion')).not.toBeInTheDocument();
74
- });
75
-
76
- it('should expand from collapsed state', () => {
77
- render(
78
- <RecordChatterPanel
79
- config={{ position: 'right', collapsible: true, defaultCollapsed: true }}
80
- items={mockItems}
81
- />,
82
- );
83
- fireEvent.click(screen.getByLabelText('Open discussion panel'));
84
- expect(screen.getByText('Discussion')).toBeInTheDocument();
85
- });
86
- });
87
-
88
- describe('inline mode (bottom)', () => {
89
- it('should render timeline in inline mode', () => {
90
- render(
91
- <RecordChatterPanel
92
- config={{ position: 'bottom', collapsible: false }}
93
- items={mockItems}
94
- />,
95
- );
96
- expect(screen.getByText('Activity')).toBeInTheDocument();
97
- expect(screen.getByText('Alice')).toBeInTheDocument();
98
- });
99
-
100
- it('should show/hide discussion toggle in inline collapsible mode', () => {
101
- render(
102
- <RecordChatterPanel
103
- config={{ position: 'bottom', collapsible: true, defaultCollapsed: true }}
104
- items={mockItems}
105
- />,
106
- );
107
- expect(screen.getByLabelText('Show Discussion (2)')).toBeInTheDocument();
108
- fireEvent.click(screen.getByLabelText('Show Discussion (2)'));
109
- expect(screen.getByText('Activity')).toBeInTheDocument();
110
- });
111
- });
112
-
113
- describe('default config', () => {
114
- it('should default to right position', () => {
115
- render(<RecordChatterPanel items={mockItems} />);
116
- expect(screen.getByText('Discussion')).toBeInTheDocument();
117
- });
118
-
119
- it('should pass feed config to embedded timeline', () => {
120
- render(
121
- <RecordChatterPanel
122
- config={{ feed: { showFilterToggle: false } }}
123
- items={mockItems}
124
- />,
125
- );
126
- expect(screen.queryByLabelText('Filter activity')).not.toBeInTheDocument();
127
- });
128
- });
129
-
130
- describe('left sidebar mode', () => {
131
- it('should render with border-r in left position', () => {
132
- const { container } = render(
133
- <RecordChatterPanel config={{ position: 'left' }} items={mockItems} />,
134
- );
135
- const panel = container.firstChild as HTMLElement;
136
- expect(panel).toHaveClass('border-r');
137
- });
138
- });
139
-
140
- describe('right sidebar width', () => {
141
- it('should apply configured width via style', () => {
142
- const { container } = render(
143
- <RecordChatterPanel config={{ position: 'right', width: '400px' }} items={mockItems} />,
144
- );
145
- const panel = container.firstChild as HTMLElement;
146
- expect(panel.style.width).toBe('400px');
147
- });
148
- });
149
-
150
- describe('collapsible=false', () => {
151
- it('should not show collapse button when collapsible is false', () => {
152
- render(
153
- <RecordChatterPanel
154
- config={{ position: 'right', collapsible: false }}
155
- items={mockItems}
156
- />,
157
- );
158
- expect(screen.getByText('Discussion')).toBeInTheDocument();
159
- expect(screen.queryByLabelText('Close discussion panel')).not.toBeInTheDocument();
160
- });
161
- });
162
-
163
- describe('sidebar timeline styling', () => {
164
- it('should pass border-0 shadow-none to embedded timeline', () => {
165
- const { container } = render(
166
- <RecordChatterPanel config={{ position: 'right' }} items={mockItems} />,
167
- );
168
- // The RecordActivityTimeline renders a Card; in sidebar mode it gets border-0 shadow-none
169
- const card = container.querySelector('.border-0.shadow-none');
170
- expect(card).toBeInTheDocument();
171
- });
172
- });
173
-
174
- describe('callback passthrough', () => {
175
- it('should forward onAddComment to embedded timeline', () => {
176
- const onAddComment = vi.fn().mockResolvedValue(undefined);
177
- render(
178
- <RecordChatterPanel
179
- config={{ position: 'right' }}
180
- items={[]}
181
- onAddComment={onAddComment}
182
- />,
183
- );
184
- expect(screen.getByPlaceholderText(/Leave a comment/)).toBeInTheDocument();
185
- });
186
- });
187
-
188
- describe('inline collapsible buttons', () => {
189
- it('should show "Show Discussion (N)" when collapsed inline', () => {
190
- render(
191
- <RecordChatterPanel
192
- config={{ position: 'bottom', collapsible: true, defaultCollapsed: true }}
193
- items={mockItems}
194
- />,
195
- );
196
- expect(screen.getByText('Show Discussion (2)')).toBeInTheDocument();
197
- });
198
-
199
- it('should show "Hide discussion" button when expanded inline', () => {
200
- render(
201
- <RecordChatterPanel
202
- config={{ position: 'bottom', collapsible: true }}
203
- items={mockItems}
204
- />,
205
- );
206
- expect(screen.getByLabelText('Hide discussion')).toBeInTheDocument();
207
- });
208
-
209
- it('should toggle between collapsed and expanded inline', () => {
210
- render(
211
- <RecordChatterPanel
212
- config={{ position: 'bottom', collapsible: true, defaultCollapsed: true }}
213
- items={mockItems}
214
- />,
215
- );
216
- // Collapsed
217
- expect(screen.getByLabelText('Show Discussion (2)')).toBeInTheDocument();
218
- fireEvent.click(screen.getByLabelText('Show Discussion (2)'));
219
- // Expanded
220
- expect(screen.getByText('Activity')).toBeInTheDocument();
221
- // Click hide
222
- fireEvent.click(screen.getByLabelText('Hide discussion'));
223
- // Collapsed again
224
- expect(screen.getByLabelText('Show Discussion (2)')).toBeInTheDocument();
225
- });
226
- });
227
-
228
- describe('collapseWhenEmpty', () => {
229
- it('should auto-collapse when empty and collapseWhenEmpty is true (inline mode)', () => {
230
- render(
231
- <RecordChatterPanel
232
- config={{ position: 'bottom', collapsible: true }}
233
- collapseWhenEmpty
234
- items={[]}
235
- />,
236
- );
237
- // Should be collapsed because items is empty
238
- expect(screen.getByLabelText('Show Discussion (0)')).toBeInTheDocument();
239
- });
240
-
241
- it('should not auto-collapse when items exist and collapseWhenEmpty is true', () => {
242
- render(
243
- <RecordChatterPanel
244
- config={{ position: 'bottom', collapsible: true }}
245
- collapseWhenEmpty
246
- items={mockItems}
247
- />,
248
- );
249
- // Should be expanded because there are items
250
- expect(screen.getByText('Activity')).toBeInTheDocument();
251
- });
252
-
253
- it('should auto-collapse sidebar when empty and collapseWhenEmpty is true', () => {
254
- render(
255
- <RecordChatterPanel
256
- config={{ position: 'right', collapsible: true }}
257
- collapseWhenEmpty
258
- items={[]}
259
- />,
260
- );
261
- // Should be collapsed
262
- expect(screen.getByLabelText('Open discussion panel')).toBeInTheDocument();
263
- });
264
- });
265
- });