Skip to content

feat: Airtable-style Comments & Activity Timeline rendering components#726

Merged
hotlong merged 3 commits intomainfrom
copilot/add-comments-activity-timeline-component
Feb 22, 2026
Merged

feat: Airtable-style Comments & Activity Timeline rendering components#726
hotlong merged 3 commits intomainfrom
copilot/add-comments-activity-timeline-component

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 22, 2026

Implements the React UI layer for the Feed/Chatter protocol defined in @objectstack/spec. Adds 8 new components to @object-ui/plugin-detail and 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 with oldDisplayValue / newDisplayValue
  • Mention — @mention targeting user/team/group with text offset
  • Reaction — emoji reaction with count + current-user state
  • RecordSubscription — notification subscription state per record

P0 — Core

  • RecordActivityTimeline — unified timeline renderer; filter dropdown (all/comments_only/changes_only/tasks_only), cursor pagination via hasMore/onLoadMore, actor avatar + source + timestamp, inline comment input
  • RecordChatterPanel — consumes RecordChatterComponentProps; supports position: right|left|bottom, collapsible/defaultCollapsed, embeds RecordActivityTimeline via feed sub-config
  • CommentInput — textarea + submit with Ctrl+Enter
  • FieldChangeItem — renders FieldChangeEntry as Label: ~~old~~ → new

P1 — Mentions & Notifications

  • MentionAutocomplete — filtered dropdown for @mention with createMentionFromSuggestion() helper
  • SubscriptionToggle — bell icon toggle consuming RecordSubscription

P2 — Reactions & Threading

  • ReactionPicker — existing reaction badges + emoji picker popover
  • ThreadedReplies — collapsible reply thread with inline reply input

Usage

<RecordChatterPanel
  config={{ position: 'right', collapsible: true, feed: { enableReactions: true, enableThreading: true } }}
  items={feedItems}
  hasMore={hasMore}
  onLoadMore={loadMore}
  onAddComment={postComment}
  subscription={{ recordId: '1', subscribed: true }}
  onToggleSubscription={toggleSub}
/>

Tests

67 new tests across 8 test files. 314 total passing in plugin-detail + types.

ROADMAP updated under P1.5.

Original prompt

This section details on the original issue you should resolve

<issue_title>UI 实现:Airtable 风格的 Comments & Activity Timeline 渲染组件</issue_title>
<issue_description>

背景

@objectstack/spec 协议层已在 objectstack-ai/spec#731完成了所有 Feed/Comment/Chatter 协议的定义,包括:

  • FeedItemSchema / MentionSchema / ReactionSchema / FieldChangeEntrySchema 数据模型 (src/data/feed.zod.ts)
  • RecordSubscriptionSchema 通知订阅协议 (src/data/subscription.zod.ts)
  • RecordActivityProps 增强版(unifiedTimeline, filterMode, commentInput, mentions, reactions, threading, subscriptionToggle)
  • RecordChatterProps 完整定义(替代原来的 EmptyProps)— position/width/collapsible/feed
  • IFeedService 服务契约 + Client SDK feed.* 命名空间
  • ✅ Feed CRUD / Reactions / Subscribe API 端点约定
  • ✅ 完整测试覆盖(feed.test.ts, subscription.test.ts, client.feed.test.ts, protocol-feed.test.ts)

当前 ObjectUI 缺失的是这些协议的 React 渲染实现。

目标

实现 Airtable/Salesforce 风格的 Activity Feed 侧边面板 UI 组件,消费 @objectstack/spec 中已定义的 Feed/Chatter 协议。

参考截图

对标 Airtable 的右侧 Activity 面板(评论 + 字段变更历史 + @Mentions + 通知订阅铃铛)

开发任务

P0 — 核心组件

  • RecordActivityTimeline 组件 — 统一时间线渲染器

    • 根据 FeedItemType 渲染不同样式的 feed item(comment / field_change / task / event / system 等)
    • 支持 unifiedTimeline 模式:评论与字段变更混排(Airtable 风格)
    • 支持 filterMode 切换:all / comments_only / changes_only / tasks_only 下拉过滤
    • 支持分页加载(limit + cursor/hasMore)
    • Actor 头像 + 名称 + 来源 + 时间戳显示
  • RecordChatterPanel 组件 — 侧边/行内/抽屉面板

    • 消费 RecordChatterProps 协议(position: sidebar/inline/drawer, width, collapsible, defaultCollapsed)
    • 内嵌 RecordActivityTimeline 作为 feed 子配置
    • 可折叠/展开
  • CommentInput 组件 — 评论输入框

    • 底部 "Leave a comment" 富文本输入
    • 调用 client.feed.create() 提交评论
    • 支持 Markdown 基本格式
  • FieldChangeItem 组件 — 字段变更历史条目

    • 渲染 FieldChangeEntrySchema:字段名 → 旧值 → 新值
    • 支持 oldDisplayValue / newDisplayValue 人类可读展示

P1 — @Mention & 通知

  • MentionAutocomplete 组件 — @提及自动完成

    • 在 CommentInput 中输入 @ 时弹出用户/团队搜索下拉
    • 生成 MentionSchema 数据(type, id, name, offset, length)
    • 渲染已插入的 mention 为高亮链接
  • SubscriptionToggle 组件 — 铃铛订阅开关

    • 消费 RecordSubscriptionSchema
    • 调用 client.feed.subscribe() / client.feed.unsubscribe()
    • 铃铛图标 + active/inactive 状态

P2 — Reactions & Threading

  • ReactionPicker 组件 — Emoji 反应选择器

    • 显示已有 reactions(emoji + count)
    • 点击添加/移除 reaction
    • 调用 client.feed.addReaction() / client.feed.removeReaction()
  • ThreadedReplies 组件 — 评论回复线程

    • 根据 parentId / replyCount 展示回复折叠
    • 点击展开子评论

P3 — 增强

  • Comment 编辑/删除 — 已有评论的 edit/delete 操作
  • 文件附件 — 评论中附加文件
  • 富文本渲染 — Markdown body 渲染为 HTML

技术方案

  • 组件放置位置建议:packages/plugin-detail/ 或新建 packages/plugin-feed/
  • 使用 Shadcn UI + Tailwind CSS 实现
  • 通过 @objectstack/client-react hooks 调用 Feed API
  • 消费 @objectstack/spec 中的 Zod Schema 进行类型推导

协议参考

协议文件 位置 说明
FeedItemSchema @objectstack/specsrc/data/feed.zod.ts Feed 数据模型
RecordSubscriptionSchema @objectstack/specsrc/data/subscription.zod.ts 通知订阅
RecordActivityProps @objectstack/specsrc/ui/component.zod.ts L94-119 Activity 组件配置
RecordChatterProps @objectstack/specsrc/ui/component.zod.ts L121-134 Chatter 面板配置
IFeedService @objectstack/spec → contracts 服务契约接口
Client SDK @objectstack/clientfeed.* API 调用层

关联

  • 上游协议 Issue: objectstack-ai/spec#731(✅ 协议已完成)
  • 示例集成位置: examples/crm/ — Lead Detail 页面已声明 record:activity 组件

建议开发流程

  1. 每个组件完成后运行测试(pnpm test
  2. 完成后更新 roadmap
  3. 建议分 PR 提交:P0 核心组件 → P1 Mention & 订阅 → P2 Reactions</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 22, 2026 2:43pm
objectui-demo Ready Ready Preview, Comment Feb 22, 2026 2:43pm
objectui-storybook Ready Ready Preview, Comment Feb 22, 2026 2:43pm

Request Review

…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>
Copilot AI changed the title [WIP] Add UI implementation for Airtable style comments and activity timeline feat: Airtable-style Comments & Activity Timeline rendering components Feb 22, 2026
Copilot AI requested a review from hotlong February 22, 2026 14:32
@hotlong hotlong marked this pull request as ready for review February 22, 2026 15:26
Copilot AI review requested due to automatic review settings February 22, 2026 15:26
@hotlong hotlong merged commit 0a007ab into main Feb 22, 2026
6 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-detail with 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)

Comment on lines +14 to +17
PlusCircle,
Trash2,
MessageSquare,
ArrowRightLeft,
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
PlusCircle,
Trash2,
MessageSquare,
ArrowRightLeft,
MessageSquare,

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +101
{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>
)}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
const onSubmit = vi.fn();
render(<CommentInput onSubmit={onSubmit} disabled />);
expect(screen.getByPlaceholderText('Leave a comment…')).toBeDisabled();
});
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
});
});
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');
});

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +108
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();
});
});
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
fireEvent.click(screen.getByLabelText('Submit comment'));
expect(onAdd).toHaveBeenCalledWith('New comment');
});

Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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');
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI 实现:Airtable 风格的 Comments & Activity Timeline 渲染组件

3 participants