Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…P1/P2) - Add FeedItem, FieldChangeEntry, Mention, Reaction, RecordSubscription types - Add RecordActivityTimeline component with unified timeline, filtering, pagination - Add RecordChatterPanel component with sidebar/inline/drawer modes - Add CommentInput component - Add FieldChangeItem component for field change history - Add MentionAutocomplete component for @mention suggestions - Add SubscriptionToggle component for bell notification toggle - Add ReactionPicker component for emoji reactions - Add ThreadedReplies component for comment threading - Add comprehensive tests (67 new tests, 185 total passing) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…on status Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request implements the React UI layer for the Feed/Chatter protocol, delivering Airtable-style Comments & Activity Timeline components as part of ObjectUI's P1.5 feature set. The implementation adds a complete set of UI components to render feed items, field changes, mentions, reactions, and threaded discussions, all properly aligned with the @objectstack/spec protocol.
Changes:
- Added 6 new protocol-aligned types to
@object-ui/types(FeedItem, FeedItemType, FieldChangeEntry, Mention, Reaction, RecordSubscription) - Implemented 8 production-ready React components in
@object-ui/plugin-detailwith Shadcn UI patterns - Delivered 67 comprehensive tests achieving good coverage of component behavior
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/types/src/views.ts | Adds 6 Feed/Chatter protocol type definitions (lines 259-370) aligned with @objectstack/spec |
| packages/types/src/index.ts | Exports new Feed/Chatter types (FeedItemType, FeedItem, FieldChangeEntry, Mention, Reaction, RecordSubscription) |
| packages/plugin-detail/src/RecordActivityTimeline.tsx | Unified timeline renderer with filtering, pagination, reactions, threading; renders 7 feed item types |
| packages/plugin-detail/src/RecordChatterPanel.tsx | Sidebar/inline/drawer panel wrapper with collapsible support; embeds RecordActivityTimeline |
| packages/plugin-detail/src/CommentInput.tsx | Reusable comment textarea with submit button and Ctrl+Enter shortcut support |
| packages/plugin-detail/src/FieldChangeItem.tsx | Renders field change history as "Label: old → new" with human-readable display values |
| packages/plugin-detail/src/MentionAutocomplete.tsx | @mention autocomplete dropdown with filtering and avatar display |
| packages/plugin-detail/src/SubscriptionToggle.tsx | Bell icon notification subscription toggle consuming RecordSubscription |
| packages/plugin-detail/src/ReactionPicker.tsx | Emoji reaction selector with existing reaction display and picker popover |
| packages/plugin-detail/src/ThreadedReplies.tsx | Collapsible comment reply threading with inline reply input |
| packages/plugin-detail/src/index.tsx | Exports all 8 new components and their corresponding prop types |
| packages/plugin-detail/src/tests/*.test.tsx | 8 test files with 67 new tests covering component rendering, interactions, and edge cases |
| ROADMAP.md | Documents P1.5 completion with component list and test counts (line 16, lines 133-142) |
| PlusCircle, | ||
| Trash2, | ||
| MessageSquare, | ||
| ArrowRightLeft, |
There was a problem hiding this comment.
Unused imports detected: PlusCircle, Trash2, and ArrowRightLeft are imported from lucide-react but are not used anywhere in this component. These should be removed to keep the codebase clean.
| PlusCircle, | |
| Trash2, | |
| MessageSquare, | |
| ArrowRightLeft, | |
| MessageSquare, |
| {showPicker && ( | ||
| <div | ||
| className="absolute bottom-full mb-1 left-0 bg-popover border rounded-md shadow-md z-50 p-1.5 flex gap-1" | ||
| role="listbox" | ||
| aria-label="Emoji picker" | ||
| > | ||
| {emojiOptions.map((emoji) => ( | ||
| <button | ||
| key={emoji} | ||
| type="button" | ||
| className="hover:bg-accent rounded p-1 text-base transition-colors" | ||
| onClick={() => handleReaction(emoji)} | ||
| role="option" | ||
| aria-selected={reactions.some(r => r.emoji === emoji && r.reacted)} | ||
| > | ||
| {emoji} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The emoji picker popup does not close when clicking outside of it. Consider adding a click-away handler (e.g., using useEffect with document click listener or a library like react-use's useClickAway) to improve UX by automatically closing the picker when the user clicks elsewhere.
| const onSubmit = vi.fn(); | ||
| render(<CommentInput onSubmit={onSubmit} disabled />); | ||
| expect(screen.getByPlaceholderText('Leave a comment…')).toBeDisabled(); | ||
| }); |
There was a problem hiding this comment.
Missing test coverage for the Ctrl+Enter keyboard shortcut functionality. The component supports Ctrl+Enter to submit (lines 49-57 in CommentInput.tsx), but there's no test verifying this behavior. Consider adding a test that simulates the keyDown event with ctrlKey=true and Enter key to ensure the shortcut works correctly.
| }); | |
| }); | |
| it('should submit when pressing Ctrl+Enter', () => { | |
| const onSubmit = vi.fn().mockResolvedValue(undefined); | |
| render(<CommentInput onSubmit={onSubmit} />); | |
| const textarea = screen.getByPlaceholderText('Leave a comment…'); | |
| fireEvent.change(textarea, { | |
| target: { value: 'Shortcut submit' }, | |
| }); | |
| fireEvent.keyDown(textarea, { | |
| key: 'Enter', | |
| code: 'Enter', | |
| ctrlKey: true, | |
| }); | |
| expect(onSubmit).toHaveBeenCalledWith('Shortcut submit'); | |
| }); |
| describe('ThreadedReplies', () => { | ||
| it('should render reply count toggle', () => { | ||
| render( | ||
| <ThreadedReplies parentItem={parentItem} replies={mockReplies} />, | ||
| ); | ||
| expect(screen.getByText('2 replies')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should use singular "reply" for one reply', () => { | ||
| render( | ||
| <ThreadedReplies parentItem={parentItem} replies={[mockReplies[0]]} />, | ||
| ); | ||
| expect(screen.getByText('1 reply')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should not render replies by default (collapsed)', () => { | ||
| render( | ||
| <ThreadedReplies parentItem={parentItem} replies={mockReplies} />, | ||
| ); | ||
| expect(screen.queryByText('First reply')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should expand replies when toggle is clicked', () => { | ||
| render( | ||
| <ThreadedReplies parentItem={parentItem} replies={mockReplies} />, | ||
| ); | ||
| fireEvent.click(screen.getByText('2 replies')); | ||
| expect(screen.getByText('First reply')).toBeInTheDocument(); | ||
| expect(screen.getByText('Second reply')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should show reply authors', () => { | ||
| render( | ||
| <ThreadedReplies parentItem={parentItem} replies={mockReplies} />, | ||
| ); | ||
| fireEvent.click(screen.getByText('2 replies')); | ||
| expect(screen.getByText('Bob')).toBeInTheDocument(); | ||
| expect(screen.getByText('Charlie')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should show reply input when onAddReply and showReplyInput', () => { | ||
| const onAdd = vi.fn(); | ||
| render( | ||
| <ThreadedReplies parentItem={parentItem} replies={[]} onAddReply={onAdd} showReplyInput />, | ||
| ); | ||
| expect(screen.getByPlaceholderText('Reply…')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should call onAddReply with parentId and text', () => { | ||
| const onAdd = vi.fn().mockResolvedValue(undefined); | ||
| render( | ||
| <ThreadedReplies parentItem={parentItem} replies={[]} onAddReply={onAdd} showReplyInput />, | ||
| ); | ||
| const input = screen.getByPlaceholderText('Reply…'); | ||
| fireEvent.change(input, { target: { value: 'My reply' } }); | ||
| fireEvent.click(screen.getByLabelText('Send reply')); | ||
| expect(onAdd).toHaveBeenCalledWith('p1', 'My reply'); | ||
| }); | ||
|
|
||
| it('should return null when no replies and no reply input', () => { | ||
| const { container } = render( | ||
| <ThreadedReplies parentItem={parentItem} replies={[]} showReplyInput={false} />, | ||
| ); | ||
| expect(container.firstChild).toBeNull(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Missing test coverage for the Ctrl+Enter keyboard shortcut functionality. The ThreadedReplies component supports Ctrl+Enter to submit replies (lines 72-80 in ThreadedReplies.tsx), but there's no test verifying this behavior. Consider adding a test that simulates the keyDown event with ctrlKey=true and Enter key.
| fireEvent.click(screen.getByLabelText('Submit comment')); | ||
| expect(onAdd).toHaveBeenCalledWith('New comment'); | ||
| }); | ||
|
|
There was a problem hiding this comment.
Missing test coverage for the Ctrl+Enter keyboard shortcut functionality. The RecordActivityTimeline component supports Ctrl+Enter to submit comments (lines 203-211 in RecordActivityTimeline.tsx), but there's no test verifying this behavior. Consider adding a test that simulates the keyDown event with ctrlKey=true and Enter key.
| it('should submit comment on Ctrl+Enter shortcut', () => { | |
| const onAdd = vi.fn().mockResolvedValue(undefined); | |
| render( | |
| <RecordActivityTimeline items={[]} onAddComment={onAdd} />, | |
| ); | |
| const input = screen.getByPlaceholderText(/Leave a comment/); | |
| fireEvent.change(input, { | |
| target: { value: 'New comment via shortcut' }, | |
| }); | |
| fireEvent.keyDown(input, { | |
| key: 'Enter', | |
| code: 'Enter', | |
| ctrlKey: true, | |
| }); | |
| expect(onAdd).toHaveBeenCalledWith('New comment via shortcut'); | |
| }); |
Implements the React UI layer for the Feed/Chatter protocol defined in
@objectstack/spec. Adds 8 new components to@object-ui/plugin-detailand 6 new types to@object-ui/types.Types (
@object-ui/types)FeedItem/FeedItemType— unified feed item model (comment, field_change, task, event, system, email, call)FieldChangeEntry— field-level change witholdDisplayValue/newDisplayValueMention— @mention targeting user/team/group with text offsetReaction— emoji reaction with count + current-user stateRecordSubscription— notification subscription state per recordP0 — Core
RecordActivityTimeline— unified timeline renderer; filter dropdown (all/comments_only/changes_only/tasks_only), cursor pagination viahasMore/onLoadMore, actor avatar + source + timestamp, inline comment inputRecordChatterPanel— consumesRecordChatterComponentProps; supportsposition: right|left|bottom,collapsible/defaultCollapsed, embedsRecordActivityTimelineviafeedsub-configCommentInput— textarea + submit with Ctrl+EnterFieldChangeItem— rendersFieldChangeEntryasLabel: ~~old~~ → newP1 — Mentions & Notifications
MentionAutocomplete— filtered dropdown for @mention withcreateMentionFromSuggestion()helperSubscriptionToggle— bell icon toggle consumingRecordSubscriptionP2 — Reactions & Threading
ReactionPicker— existing reaction badges + emoji picker popoverThreadedReplies— collapsible reply thread with inline reply inputUsage
Tests
67 new tests across 8 test files. 314 total passing in plugin-detail + types.
ROADMAP updated under P1.5.
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.