@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,429 +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 * as React from 'react';
10
- import { cn, Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui/components';
11
- import {
12
- Activity,
13
- Edit,
14
- PlusCircle,
15
- Trash2,
16
- MessageSquare,
17
- ArrowRightLeft,
18
- Calendar,
19
- CheckSquare,
20
- Zap,
21
- Mail,
22
- Phone,
23
- ChevronDown,
24
- Loader2,
25
- } from 'lucide-react';
26
- import type { FeedItem, FeedItemType, RecordActivityComponentProps, RecordSubscription } from '@object-ui/types';
27
- import { FieldChangeItem } from './FieldChangeItem';
28
- import { ReactionPicker } from './ReactionPicker';
29
- import { ThreadedReplies } from './ThreadedReplies';
30
- import { SubscriptionToggle } from './SubscriptionToggle';
31
-
32
- export type FeedFilterMode = 'all' | 'comments_only' | 'changes_only' | 'tasks_only';
33
-
34
- export interface RecordActivityTimelineProps {
35
- /** Feed items to display */
36
- items: FeedItem[];
37
- /** Activity configuration from RecordActivityComponentProps */
38
- config?: RecordActivityComponentProps;
39
- /** Filter mode for the timeline */
40
- filterMode?: FeedFilterMode;
41
- /** Called when filter mode changes */
42
- onFilterChange?: (mode: FeedFilterMode) => void;
43
- /** Whether there are more items to load */
44
- hasMore?: boolean;
45
- /** Called when user wants to load more items */
46
- onLoadMore?: () => void | Promise<void>;
47
- /** Loading state */
48
- loading?: boolean;
49
- /** Called when a comment is submitted */
50
- onAddComment?: (text: string) => void | Promise<void>;
51
- /** Called when a reply is submitted */
52
- onAddReply?: (parentId: string | number, text: string) => void | Promise<void>;
53
- /** Called when user toggles a reaction */
54
- onToggleReaction?: (itemId: string | number, emoji: string) => void | Promise<void>;
55
- /** Subscription state */
56
- subscription?: RecordSubscription;
57
- /** Called when user toggles subscription */
58
- onToggleSubscription?: (subscribed: boolean) => void | Promise<void>;
59
- /** When true, collapse to only the comment input when there are no items */
60
- collapseWhenEmpty?: boolean;
61
- className?: string;
62
- }
63
-
64
- const FEED_TYPE_ICONS: Record<FeedItemType, React.ElementType> = {
65
- comment: MessageSquare,
66
- field_change: Edit,
67
- task: CheckSquare,
68
- event: Calendar,
69
- system: Zap,
70
- email: Mail,
71
- call: Phone,
72
- };
73
-
74
- const FEED_TYPE_COLORS: Record<FeedItemType, string> = {
75
- comment: 'bg-purple-100 text-purple-600',
76
- field_change: 'bg-blue-100 text-blue-600',
77
- task: 'bg-green-100 text-green-600',
78
- event: 'bg-amber-100 text-amber-600',
79
- system: 'bg-gray-100 text-gray-600',
80
- email: 'bg-indigo-100 text-indigo-600',
81
- call: 'bg-teal-100 text-teal-600',
82
- };
83
-
84
- const FILTER_OPTIONS: { value: FeedFilterMode; label: string }[] = [
85
- { value: 'all', label: 'All Activity' },
86
- { value: 'comments_only', label: 'Comments Only' },
87
- { value: 'changes_only', label: 'Field Changes' },
88
- { value: 'tasks_only', label: 'Tasks Only' },
89
- ];
90
-
91
- function formatTimestamp(timestamp: string): string {
92
- try {
93
- const date = new Date(timestamp);
94
- const now = new Date();
95
- const diffMs = now.getTime() - date.getTime();
96
- const diffMins = Math.floor(diffMs / 60000);
97
-
98
- if (diffMins < 1) return 'just now';
99
- if (diffMins < 60) return `${diffMins}m ago`;
100
- const diffHours = Math.floor(diffMins / 60);
101
- if (diffHours < 24) return `${diffHours}h ago`;
102
- const diffDays = Math.floor(diffHours / 24);
103
- if (diffDays < 7) return `${diffDays}d ago`;
104
- return date.toLocaleDateString();
105
- } catch {
106
- return timestamp;
107
- }
108
- }
109
-
110
- function filterItems(items: FeedItem[], mode: FeedFilterMode): FeedItem[] {
111
- switch (mode) {
112
- case 'comments_only':
113
- return items.filter((i) => i.type === 'comment');
114
- case 'changes_only':
115
- return items.filter((i) => i.type === 'field_change');
116
- case 'tasks_only':
117
- return items.filter((i) => i.type === 'task');
118
- default:
119
- return items;
120
- }
121
- }
122
-
123
- /**
124
- * RecordActivityTimeline — Unified timeline renderer for Airtable-style activity feeds.
125
- *
126
- * Renders different feed item types (comment, field_change, task, event, system, etc.)
127
- * in a unified timeline. Supports filtering, pagination, reactions, and threading.
128
- *
129
- * Aligned with @objectstack/spec RecordActivityProps.
130
- */
131
- export const RecordActivityTimeline: React.FC<RecordActivityTimelineProps> = ({
132
- items,
133
- config,
134
- filterMode: controlledFilter,
135
- onFilterChange,
136
- hasMore = false,
137
- onLoadMore,
138
- loading: _loading = false,
139
- onAddComment,
140
- onAddReply,
141
- onToggleReaction,
142
- subscription,
143
- onToggleSubscription,
144
- collapseWhenEmpty = false,
145
- className,
146
- }) => {
147
- const [internalFilter, setInternalFilter] = React.useState<FeedFilterMode>('all');
148
- const [commentText, setCommentText] = React.useState('');
149
- const [isSubmitting, setIsSubmitting] = React.useState(false);
150
- const [isLoadingMore, setIsLoadingMore] = React.useState(false);
151
-
152
- const activeFilter = controlledFilter ?? internalFilter;
153
- const showFilter = config?.showFilterToggle !== false;
154
- const showCommentInput = config?.showCommentInput !== false && !!onAddComment;
155
- const enableReactions = config?.enableReactions ?? false;
156
- const enableThreading = config?.enableThreading ?? false;
157
- const showSubscription = config?.showSubscriptionToggle ?? false;
158
-
159
- const filtered = React.useMemo(
160
- () => filterItems(items, activeFilter),
161
- [items, activeFilter],
162
- );
163
-
164
- // Group replies by parentId
165
- const rootItems = React.useMemo(() => {
166
- if (!enableThreading) return filtered;
167
- return filtered.filter((i) => !i.parentId);
168
- }, [filtered, enableThreading]);
169
-
170
- const repliesByParent = React.useMemo(() => {
171
- if (!enableThreading) return new Map<string | number, FeedItem[]>();
172
- const map = new Map<string | number, FeedItem[]>();
173
- for (const item of filtered) {
174
- if (item.parentId) {
175
- const existing = map.get(item.parentId) ?? [];
176
- existing.push(item);
177
- map.set(item.parentId, existing);
178
- }
179
- }
180
- return map;
181
- }, [filtered, enableThreading]);
182
-
183
- const handleFilterChange = React.useCallback(
184
- (mode: FeedFilterMode) => {
185
- if (onFilterChange) {
186
- onFilterChange(mode);
187
- } else {
188
- setInternalFilter(mode);
189
- }
190
- },
191
- [onFilterChange],
192
- );
193
-
194
- const handleAddComment = React.useCallback(async () => {
195
- const text = commentText.trim();
196
- if (!text || !onAddComment) return;
197
- setIsSubmitting(true);
198
- try {
199
- await onAddComment(text);
200
- setCommentText('');
201
- } finally {
202
- setIsSubmitting(false);
203
- }
204
- }, [commentText, onAddComment]);
205
-
206
- const handleKeyDown = React.useCallback(
207
- (e: React.KeyboardEvent) => {
208
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
209
- e.preventDefault();
210
- handleAddComment();
211
- }
212
- },
213
- [handleAddComment],
214
- );
215
-
216
- const handleLoadMore = React.useCallback(async () => {
217
- if (!onLoadMore) return;
218
- setIsLoadingMore(true);
219
- try {
220
- await onLoadMore();
221
- } finally {
222
- setIsLoadingMore(false);
223
- }
224
- }, [onLoadMore]);
225
-
226
- return (
227
- <Card className={cn('', className)}>
228
- <CardHeader>
229
- <div className="flex items-center justify-between">
230
- <CardTitle className="flex items-center gap-2 text-base">
231
- <Activity className="h-4 w-4" />
232
- Activity
233
- <span className="text-sm font-normal text-muted-foreground">
234
- ({filtered.length})
235
- </span>
236
- </CardTitle>
237
- <div className="flex items-center gap-1">
238
- {showSubscription && subscription && (
239
- <SubscriptionToggle
240
- subscription={subscription}
241
- onToggle={onToggleSubscription}
242
- />
243
- )}
244
- </div>
245
- </div>
246
- </CardHeader>
247
- <CardContent className="space-y-4">
248
- {/* Filter dropdown */}
249
- {showFilter && (
250
- <div className="flex items-center gap-2">
251
- <select
252
- className="rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
253
- value={activeFilter}
254
- onChange={(e) => handleFilterChange(e.target.value as FeedFilterMode)}
255
- aria-label="Filter activity"
256
- >
257
- {FILTER_OPTIONS.map((opt) => (
258
- <option key={opt.value} value={opt.value}>
259
- {opt.label}
260
- </option>
261
- ))}
262
- </select>
263
- </div>
264
- )}
265
-
266
- {/* Comment Input */}
267
- {showCommentInput && (
268
- <div className="flex gap-2">
269
- <textarea
270
- className="flex-1 min-h-[60px] rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-none"
271
- placeholder="Leave a comment… (Ctrl+Enter to submit)"
272
- value={commentText}
273
- onChange={(e) => setCommentText(e.target.value)}
274
- onKeyDown={handleKeyDown}
275
- disabled={isSubmitting}
276
- />
277
- <Button
278
- size="icon"
279
- variant="default"
280
- onClick={handleAddComment}
281
- disabled={!commentText.trim() || isSubmitting}
282
- className="shrink-0 self-end"
283
- aria-label="Submit comment"
284
- >
285
- <MessageSquare className="h-4 w-4" />
286
- </Button>
287
- </div>
288
- )}
289
-
290
- {/* Timeline */}
291
- {filtered.length === 0 ? (
292
- collapseWhenEmpty ? null : (
293
- <p className="text-sm text-muted-foreground text-center py-4">
294
- No activity recorded
295
- </p>
296
- )
297
- ) : (
298
- <div className="relative">
299
- {/* Timeline line */}
300
- <div className="absolute left-4 top-2 bottom-2 w-px bg-border" />
301
-
302
- <div className="space-y-4">
303
- {rootItems.map((item) => {
304
- const Icon = FEED_TYPE_ICONS[item.type] || Zap;
305
- const colorClass =
306
- FEED_TYPE_COLORS[item.type] || 'bg-gray-100 text-gray-600';
307
- const replies = repliesByParent.get(item.id) ?? [];
308
-
309
- return (
310
- <div key={item.id}>
311
- <div className="flex gap-3 relative">
312
- {/* Icon */}
313
- <div
314
- className={cn(
315
- 'shrink-0 h-8 w-8 rounded-full flex items-center justify-center z-10',
316
- colorClass,
317
- )}
318
- >
319
- {item.actorAvatarUrl ? (
320
- <img
321
- src={item.actorAvatarUrl}
322
- alt={item.actor}
323
- className="h-8 w-8 rounded-full object-cover"
324
- />
325
- ) : (
326
- <Icon className="h-3.5 w-3.5" />
327
- )}
328
- </div>
329
- {/* Content */}
330
- <div className="flex-1 min-w-0 pt-1">
331
- <div className="flex items-center gap-2 mb-0.5">
332
- <span className="text-sm font-medium">{item.actor}</span>
333
- {item.source && (
334
- <span className="text-xs text-muted-foreground">
335
- via {item.source}
336
- </span>
337
- )}
338
- <span className="text-xs text-muted-foreground">
339
- {formatTimestamp(item.createdAt)}
340
- </span>
341
- {item.edited && (
342
- <span className="text-xs text-muted-foreground italic">(edited)</span>
343
- )}
344
- {item.pinned && (
345
- <span className="text-xs text-amber-600">📌 Pinned</span>
346
- )}
347
- </div>
348
-
349
- {/* Body text */}
350
- {item.body && (
351
- <p className="text-sm whitespace-pre-wrap break-words text-muted-foreground">
352
- {item.body}
353
- </p>
354
- )}
355
-
356
- {/* Field changes */}
357
- {item.type === 'field_change' && item.fieldChanges && (
358
- <div className="space-y-1 mt-1">
359
- {item.fieldChanges.map((change, idx) => (
360
- <FieldChangeItem key={idx} change={change} />
361
- ))}
362
- </div>
363
- )}
364
-
365
- {/* Reactions */}
366
- {enableReactions && item.reactions && item.reactions.length > 0 && (
367
- <div className="mt-1.5">
368
- <ReactionPicker
369
- reactions={item.reactions}
370
- onToggleReaction={
371
- onToggleReaction
372
- ? (emoji) => onToggleReaction(item.id, emoji)
373
- : undefined
374
- }
375
- />
376
- </div>
377
- )}
378
-
379
- {/* Add reaction button (even if no reactions yet) */}
380
- {enableReactions && (!item.reactions || item.reactions.length === 0) && onToggleReaction && (
381
- <div className="mt-1.5">
382
- <ReactionPicker
383
- reactions={[]}
384
- onToggleReaction={(emoji) => onToggleReaction(item.id, emoji)}
385
- />
386
- </div>
387
- )}
388
- </div>
389
- </div>
390
-
391
- {/* Threading */}
392
- {enableThreading && (item.replyCount ?? 0) > 0 && (
393
- <ThreadedReplies
394
- parentItem={item}
395
- replies={replies}
396
- onAddReply={onAddReply}
397
- showReplyInput={!!onAddReply}
398
- />
399
- )}
400
- </div>
401
- );
402
- })}
403
- </div>
404
- </div>
405
- )}
406
-
407
- {/* Load More */}
408
- {hasMore && (
409
- <div className="text-center pt-2">
410
- <Button
411
- variant="ghost"
412
- size="sm"
413
- onClick={handleLoadMore}
414
- disabled={isLoadingMore}
415
- aria-label="Load more activity"
416
- >
417
- {isLoadingMore ? (
418
- <Loader2 className="h-4 w-4 animate-spin mr-1" />
419
- ) : (
420
- <ChevronDown className="h-4 w-4 mr-1" />
421
- )}
422
- Load more
423
- </Button>
424
- </div>
425
- )}
426
- </CardContent>
427
- </Card>
428
- );
429
- };
@@ -1,207 +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 * as React from 'react';
10
- import { cn, Button } from '@object-ui/components';
11
- import { MessageSquare, PanelRightOpen, PanelRightClose, X } from 'lucide-react';
12
- import type { RecordChatterComponentProps, FeedItem, RecordSubscription } from '@object-ui/types';
13
- import { RecordActivityTimeline } from './RecordActivityTimeline';
14
- import type { FeedFilterMode, RecordActivityTimelineProps } from './RecordActivityTimeline';
15
-
16
- export interface RecordChatterPanelProps {
17
- /** Chatter panel configuration from RecordChatterComponentProps */
18
- config?: RecordChatterComponentProps;
19
- /** Feed items to display in the embedded timeline */
20
- items: FeedItem[];
21
- /** Whether there are more items to load */
22
- hasMore?: boolean;
23
- /** Called when user wants to load more */
24
- onLoadMore?: () => void | Promise<void>;
25
- /** Loading state */
26
- loading?: boolean;
27
- /** Called when a comment is submitted */
28
- onAddComment?: (text: string) => void | Promise<void>;
29
- /** Called when a reply is submitted */
30
- onAddReply?: (parentId: string | number, text: string) => void | Promise<void>;
31
- /** Called when user toggles a reaction */
32
- onToggleReaction?: (itemId: string | number, emoji: string) => void | Promise<void>;
33
- /** Subscription state */
34
- subscription?: RecordSubscription;
35
- /** Called when user toggles subscription */
36
- onToggleSubscription?: (subscribed: boolean) => void | Promise<void>;
37
- /** Filter mode */
38
- filterMode?: FeedFilterMode;
39
- /** Called when filter changes */
40
- onFilterChange?: (mode: FeedFilterMode) => void;
41
- /** When true, auto-collapse panel when there are no feed items */
42
- collapseWhenEmpty?: boolean;
43
- className?: string;
44
- }
45
-
46
- /**
47
- * RecordChatterPanel — Side/inline/drawer panel for record discussions.
48
- *
49
- * Consumes RecordChatterComponentProps from the spec protocol.
50
- * Supports three positions: bottom (inline), right (sidebar), left (sidebar).
51
- * Can be collapsible/expandable.
52
- *
53
- * Embeds RecordActivityTimeline as the feed sub-component.
54
- */
55
- export const RecordChatterPanel: React.FC<RecordChatterPanelProps> = ({
56
- config,
57
- items,
58
- hasMore,
59
- onLoadMore,
60
- loading,
61
- onAddComment,
62
- onAddReply,
63
- onToggleReaction,
64
- subscription,
65
- onToggleSubscription,
66
- filterMode,
67
- onFilterChange,
68
- collapseWhenEmpty = false,
69
- className,
70
- }) => {
71
- const position = config?.position ?? 'right';
72
- const width = config?.width ?? '360px';
73
- const collapsible = config?.collapsible ?? true;
74
- const defaultCollapsed = (collapseWhenEmpty && items.length === 0) || (config?.defaultCollapsed ?? false);
75
-
76
- const [collapsed, setCollapsed] = React.useState(defaultCollapsed);
77
-
78
- const isSidebar = position === 'right' || position === 'left';
79
- const isInline = position === 'bottom';
80
-
81
- // Sidebar mode
82
- if (isSidebar) {
83
- if (collapsed && collapsible) {
84
- return (
85
- <div
86
- className={cn(
87
- 'flex items-start pt-4',
88
- position === 'right' ? 'border-l' : 'border-r',
89
- className,
90
- )}
91
- >
92
- <Button
93
- variant="ghost"
94
- size="icon"
95
- className="h-8 w-8 mx-1"
96
- onClick={() => setCollapsed(false)}
97
- aria-label="Open discussion panel"
98
- >
99
- <PanelRightOpen className="h-4 w-4" />
100
- </Button>
101
- </div>
102
- );
103
- }
104
-
105
- return (
106
- <div
107
- className={cn(
108
- 'flex flex-col overflow-hidden',
109
- position === 'right' ? 'border-l' : 'border-r',
110
- className,
111
- )}
112
- style={{ width, minWidth: width }}
113
- >
114
- {/* Header */}
115
- <div className="flex items-center justify-between px-4 py-3 border-b">
116
- <div className="flex items-center gap-2">
117
- <MessageSquare className="h-4 w-4" />
118
- <span className="text-sm font-medium">Discussion</span>
119
- </div>
120
- {collapsible && (
121
- <Button
122
- variant="ghost"
123
- size="icon"
124
- className="h-7 w-7"
125
- onClick={() => setCollapsed(true)}
126
- aria-label="Close discussion panel"
127
- >
128
- <X className="h-3.5 w-3.5" />
129
- </Button>
130
- )}
131
- </div>
132
-
133
- {/* Embedded Timeline */}
134
- <div className="flex-1 overflow-y-auto">
135
- <RecordActivityTimeline
136
- items={items}
137
- config={config?.feed}
138
- hasMore={hasMore}
139
- onLoadMore={onLoadMore}
140
- loading={loading}
141
- onAddComment={onAddComment}
142
- onAddReply={onAddReply}
143
- onToggleReaction={onToggleReaction}
144
- subscription={subscription}
145
- onToggleSubscription={onToggleSubscription}
146
- filterMode={filterMode}
147
- onFilterChange={onFilterChange}
148
- collapseWhenEmpty={collapseWhenEmpty}
149
- className="border-0 shadow-none"
150
- />
151
- </div>
152
- </div>
153
- );
154
- }
155
-
156
- // Inline / bottom mode
157
- return (
158
- <div className={cn('', className)}>
159
- {collapsible && collapsed ? (
160
- <Button
161
- variant="ghost"
162
- className="w-full justify-start gap-2 text-muted-foreground"
163
- onClick={() => setCollapsed(false)}
164
- aria-label="Show discussion"
165
- >
166
- <MessageSquare className="h-4 w-4" />
167
- <span>Show Discussion ({items.length})</span>
168
- </Button>
169
- ) : (
170
- <div>
171
- {collapsible && (
172
- <div className="flex items-center justify-between mb-2">
173
- <div className="flex items-center gap-2 text-sm font-medium">
174
- <MessageSquare className="h-4 w-4" />
175
- Discussion
176
- </div>
177
- <Button
178
- variant="ghost"
179
- size="icon"
180
- className="h-7 w-7"
181
- onClick={() => setCollapsed(true)}
182
- aria-label="Hide discussion"
183
- >
184
- <PanelRightClose className="h-3.5 w-3.5" />
185
- </Button>
186
- </div>
187
- )}
188
- <RecordActivityTimeline
189
- items={items}
190
- config={config?.feed}
191
- hasMore={hasMore}
192
- onLoadMore={onLoadMore}
193
- loading={loading}
194
- onAddComment={onAddComment}
195
- onAddReply={onAddReply}
196
- onToggleReaction={onToggleReaction}
197
- subscription={subscription}
198
- onToggleSubscription={onToggleSubscription}
199
- filterMode={filterMode}
200
- onFilterChange={onFilterChange}
201
- collapseWhenEmpty={collapseWhenEmpty}
202
- />
203
- </div>
204
- )}
205
- </div>
206
- );
207
- };