@object-ui/plugin-detail 3.1.5 → 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 (209) hide show
  1. package/CHANGELOG.md +31 -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-YGj51ozd.js → AvatarField-Dy2XGlPz.js} +16 -15
  6. package/dist/{BooleanField-CaA898Tk.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-I1A9oEGC.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-DBtluGJ1.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-B_Mnr63i.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-CddhEK9u.js → SignatureField-B1wh3f5A.js} +18 -17
  31. package/dist/{SliderField-Df5hMzNc.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.d.ts +1 -1
  40. package/dist/index.js +1741 -1504
  41. package/dist/index.umd.cjs +43 -51
  42. package/dist/packages/plugin-detail/src/ActivityTimeline.d.ts.map +1 -0
  43. package/dist/packages/plugin-detail/src/CommentAttachment.d.ts.map +1 -0
  44. package/dist/packages/plugin-detail/src/CommentInput.d.ts.map +1 -0
  45. package/dist/packages/plugin-detail/src/DetailSection.d.ts.map +1 -0
  46. package/dist/packages/plugin-detail/src/DetailTabs.d.ts.map +1 -0
  47. package/dist/packages/plugin-detail/src/DetailView.d.ts +47 -0
  48. package/dist/packages/plugin-detail/src/DetailView.d.ts.map +1 -0
  49. package/dist/packages/plugin-detail/src/DetailView.stories.d.ts.map +1 -0
  50. package/dist/packages/plugin-detail/src/DiffView.d.ts.map +1 -0
  51. package/dist/packages/plugin-detail/src/FieldChangeItem.d.ts.map +1 -0
  52. package/dist/packages/plugin-detail/src/HeaderHighlight.d.ts.map +1 -0
  53. package/dist/packages/plugin-detail/src/InlineCreateRelated.d.ts.map +1 -0
  54. package/dist/packages/plugin-detail/src/MentionAutocomplete.d.ts.map +1 -0
  55. package/dist/packages/plugin-detail/src/PointInTimeRestore.d.ts.map +1 -0
  56. package/dist/packages/plugin-detail/src/ReactionPicker.d.ts.map +1 -0
  57. package/dist/packages/plugin-detail/src/RecordActivityTimeline.d.ts.map +1 -0
  58. package/dist/packages/plugin-detail/src/RecordChatterPanel.d.ts.map +1 -0
  59. package/dist/packages/plugin-detail/src/RecordComments.d.ts.map +1 -0
  60. package/dist/packages/plugin-detail/src/RecordNavigationEnhanced.d.ts.map +1 -0
  61. package/dist/{src → packages/plugin-detail/src}/RelatedList.d.ts +8 -0
  62. package/dist/packages/plugin-detail/src/RelatedList.d.ts.map +1 -0
  63. package/dist/packages/plugin-detail/src/RelationshipGraph.d.ts.map +1 -0
  64. package/dist/packages/plugin-detail/src/RichTextCommentInput.d.ts.map +1 -0
  65. package/dist/packages/plugin-detail/src/SectionGroup.d.ts.map +1 -0
  66. package/dist/packages/plugin-detail/src/SubscriptionToggle.d.ts.map +1 -0
  67. package/dist/packages/plugin-detail/src/ThreadedReplies.d.ts.map +1 -0
  68. package/dist/packages/plugin-detail/src/autoLayout.d.ts.map +1 -0
  69. package/dist/packages/plugin-detail/src/index.d.ts.map +1 -0
  70. package/dist/packages/plugin-detail/src/useDetailTranslation.d.ts.map +1 -0
  71. package/dist/plugin-detail.css +1 -2
  72. package/dist/rolldown-runtime-DnwLefa7.js +23 -0
  73. package/dist/{src-CXr1-vVl.js → src-DyUKLvMN.js} +29788 -37711
  74. package/dist/useFieldTranslation-BRgjC1oq.js +9 -0
  75. package/package.json +34 -12
  76. package/.turbo/turbo-build.log +0 -61
  77. package/dist/AddressField-DBkEyMcG.js +0 -93
  78. package/dist/AutoNumberField-Baa191z-.js +0 -14
  79. package/dist/CodeField-BU51nl1L.js +0 -22
  80. package/dist/ColorField-Cnf6ZM7c.js +0 -37
  81. package/dist/CurrencyField-Wg-XOId2.js +0 -51
  82. package/dist/DateField-Cth1ky_m.js +0 -21
  83. package/dist/DateTimeField-B0m6FhHL.js +0 -32
  84. package/dist/EmailField-Do7qT_L_.js +0 -28
  85. package/dist/FileField-aRJAdbQb.js +0 -151
  86. package/dist/FormulaField-DTMkagFx.js +0 -14
  87. package/dist/GeolocationField-RqpHWTEv.js +0 -113
  88. package/dist/GridField-D4IH0cpo.js +0 -51
  89. package/dist/ImageField-BYCFajjr.js +0 -75
  90. package/dist/LocationField-Bi_ew9sd.js +0 -35
  91. package/dist/LookupField-BjwlDPtt.js +0 -902
  92. package/dist/NumberField-D_NucQlp.js +0 -26
  93. package/dist/ObjectField-CG-LaM65.js +0 -52
  94. package/dist/PercentField-B6sO_J3i.js +0 -63
  95. package/dist/PhoneField-CcQAWwR6.js +0 -28
  96. package/dist/QRCodeField-CEjWs-J5.js +0 -72
  97. package/dist/RichTextField-qOEJl5Ai.js +0 -32
  98. package/dist/SelectField-C8hWu3gm.js +0 -30
  99. package/dist/SummaryField-DgiFm-Cr.js +0 -19
  100. package/dist/TextAreaField-DuriTqsD.js +0 -36
  101. package/dist/TextField-CGNSl7RU.js +0 -29
  102. package/dist/TimeField-YO58ctFg.js +0 -21
  103. package/dist/UrlField-1-BMM1jn.js +0 -33
  104. package/dist/UserField-B6GqxP_S.js +0 -78
  105. package/dist/VectorField-BkEjbSt0.js +0 -36
  106. package/dist/src/ActivityTimeline.d.ts.map +0 -1
  107. package/dist/src/CommentAttachment.d.ts.map +0 -1
  108. package/dist/src/CommentInput.d.ts.map +0 -1
  109. package/dist/src/DetailSection.d.ts.map +0 -1
  110. package/dist/src/DetailTabs.d.ts.map +0 -1
  111. package/dist/src/DetailView.d.ts +0 -23
  112. package/dist/src/DetailView.d.ts.map +0 -1
  113. package/dist/src/DetailView.stories.d.ts.map +0 -1
  114. package/dist/src/DiffView.d.ts.map +0 -1
  115. package/dist/src/FieldChangeItem.d.ts.map +0 -1
  116. package/dist/src/HeaderHighlight.d.ts.map +0 -1
  117. package/dist/src/InlineCreateRelated.d.ts.map +0 -1
  118. package/dist/src/MentionAutocomplete.d.ts.map +0 -1
  119. package/dist/src/PointInTimeRestore.d.ts.map +0 -1
  120. package/dist/src/ReactionPicker.d.ts.map +0 -1
  121. package/dist/src/RecordActivityTimeline.d.ts.map +0 -1
  122. package/dist/src/RecordChatterPanel.d.ts.map +0 -1
  123. package/dist/src/RecordComments.d.ts.map +0 -1
  124. package/dist/src/RecordNavigationEnhanced.d.ts.map +0 -1
  125. package/dist/src/RelatedList.d.ts.map +0 -1
  126. package/dist/src/RelationshipGraph.d.ts.map +0 -1
  127. package/dist/src/RichTextCommentInput.d.ts.map +0 -1
  128. package/dist/src/SectionGroup.d.ts.map +0 -1
  129. package/dist/src/SubscriptionToggle.d.ts.map +0 -1
  130. package/dist/src/ThreadedReplies.d.ts.map +0 -1
  131. package/dist/src/autoLayout.d.ts.map +0 -1
  132. package/dist/src/index.d.ts.map +0 -1
  133. package/dist/src/useDetailTranslation.d.ts.map +0 -1
  134. package/src/ActivityTimeline.tsx +0 -184
  135. package/src/CommentAttachment.tsx +0 -192
  136. package/src/CommentInput.tsx +0 -81
  137. package/src/DetailSection.tsx +0 -340
  138. package/src/DetailTabs.tsx +0 -73
  139. package/src/DetailView.stories.tsx +0 -334
  140. package/src/DetailView.tsx +0 -823
  141. package/src/DiffView.tsx +0 -231
  142. package/src/FieldChangeItem.tsx +0 -46
  143. package/src/HeaderHighlight.tsx +0 -88
  144. package/src/InlineCreateRelated.tsx +0 -291
  145. package/src/MentionAutocomplete.tsx +0 -123
  146. package/src/PointInTimeRestore.tsx +0 -261
  147. package/src/ReactionPicker.tsx +0 -106
  148. package/src/RecordActivityTimeline.tsx +0 -429
  149. package/src/RecordChatterPanel.tsx +0 -207
  150. package/src/RecordComments.tsx +0 -215
  151. package/src/RecordNavigationEnhanced.tsx +0 -211
  152. package/src/RelatedList.tsx +0 -413
  153. package/src/RelationshipGraph.tsx +0 -286
  154. package/src/RichTextCommentInput.tsx +0 -348
  155. package/src/SectionGroup.tsx +0 -101
  156. package/src/SubscriptionToggle.tsx +0 -60
  157. package/src/ThreadedReplies.tsx +0 -161
  158. package/src/__tests__/ActivityTimeline.test.tsx +0 -119
  159. package/src/__tests__/ActivityTimelineFiltering.test.tsx +0 -143
  160. package/src/__tests__/CommentInput.test.tsx +0 -57
  161. package/src/__tests__/DetailSection.test.tsx +0 -490
  162. package/src/__tests__/DetailView.test.tsx +0 -694
  163. package/src/__tests__/FieldChangeItem.test.tsx +0 -119
  164. package/src/__tests__/HeaderHighlight.test.tsx +0 -213
  165. package/src/__tests__/MentionAutocomplete.test.tsx +0 -97
  166. package/src/__tests__/ReactionPicker.test.tsx +0 -113
  167. package/src/__tests__/RecordActivityTimeline.test.tsx +0 -395
  168. package/src/__tests__/RecordChatterPanel.test.tsx +0 -265
  169. package/src/__tests__/RecordComments.test.tsx +0 -96
  170. package/src/__tests__/RecordCommentsPinSearch.test.tsx +0 -133
  171. package/src/__tests__/RelatedList.test.tsx +0 -160
  172. package/src/__tests__/SectionGroup.test.tsx +0 -101
  173. package/src/__tests__/SubscriptionToggle.test.tsx +0 -84
  174. package/src/__tests__/ThreadedReplies.test.tsx +0 -212
  175. package/src/__tests__/autoLayout.test.ts +0 -228
  176. package/src/__tests__/phase12-features.test.tsx +0 -583
  177. package/src/__tests__/roadmap-features.test.tsx +0 -478
  178. package/src/autoLayout.ts +0 -128
  179. package/src/index.tsx +0 -149
  180. package/src/useDetailTranslation.ts +0 -114
  181. package/tsconfig.json +0 -18
  182. package/vite.config.ts +0 -56
  183. package/vitest.config.ts +0 -13
  184. package/vitest.setup.ts +0 -1
  185. /package/dist/{src → packages/plugin-detail/src}/ActivityTimeline.d.ts +0 -0
  186. /package/dist/{src → packages/plugin-detail/src}/CommentAttachment.d.ts +0 -0
  187. /package/dist/{src → packages/plugin-detail/src}/CommentInput.d.ts +0 -0
  188. /package/dist/{src → packages/plugin-detail/src}/DetailSection.d.ts +0 -0
  189. /package/dist/{src → packages/plugin-detail/src}/DetailTabs.d.ts +0 -0
  190. /package/dist/{src → packages/plugin-detail/src}/DetailView.stories.d.ts +0 -0
  191. /package/dist/{src → packages/plugin-detail/src}/DiffView.d.ts +0 -0
  192. /package/dist/{src → packages/plugin-detail/src}/FieldChangeItem.d.ts +0 -0
  193. /package/dist/{src → packages/plugin-detail/src}/HeaderHighlight.d.ts +0 -0
  194. /package/dist/{src → packages/plugin-detail/src}/InlineCreateRelated.d.ts +0 -0
  195. /package/dist/{src → packages/plugin-detail/src}/MentionAutocomplete.d.ts +0 -0
  196. /package/dist/{src → packages/plugin-detail/src}/PointInTimeRestore.d.ts +0 -0
  197. /package/dist/{src → packages/plugin-detail/src}/ReactionPicker.d.ts +0 -0
  198. /package/dist/{src → packages/plugin-detail/src}/RecordActivityTimeline.d.ts +0 -0
  199. /package/dist/{src → packages/plugin-detail/src}/RecordChatterPanel.d.ts +0 -0
  200. /package/dist/{src → packages/plugin-detail/src}/RecordComments.d.ts +0 -0
  201. /package/dist/{src → packages/plugin-detail/src}/RecordNavigationEnhanced.d.ts +0 -0
  202. /package/dist/{src → packages/plugin-detail/src}/RelationshipGraph.d.ts +0 -0
  203. /package/dist/{src → packages/plugin-detail/src}/RichTextCommentInput.d.ts +0 -0
  204. /package/dist/{src → packages/plugin-detail/src}/SectionGroup.d.ts +0 -0
  205. /package/dist/{src → packages/plugin-detail/src}/SubscriptionToggle.d.ts +0 -0
  206. /package/dist/{src → packages/plugin-detail/src}/ThreadedReplies.d.ts +0 -0
  207. /package/dist/{src → packages/plugin-detail/src}/autoLayout.d.ts +0 -0
  208. /package/dist/{src → packages/plugin-detail/src}/index.d.ts +0 -0
  209. /package/dist/{src → packages/plugin-detail/src}/useDetailTranslation.d.ts +0 -0
@@ -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 comment'));
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 activity')).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 activity'));
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 comment'));
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 comment'));
279
- expect(textarea).toBeDisabled();
280
- expect(screen.getByLabelText('Submit comment')).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 activity'));
294
- // Loader2 spinner should be present while loading
295
- expect(screen.getByLabelText('Load more activity')).toBeDisabled();
296
- resolveLoad!();
297
- await vi.waitFor(() => {
298
- expect(screen.getByLabelText('Load more activity')).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')).toBeInTheDocument();
108
- fireEvent.click(screen.getByLabelText('Show discussion'));
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')).toBeInTheDocument();
218
- fireEvent.click(screen.getByLabelText('Show discussion'));
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')).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')).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
- });