Skip to content

feat(activity-feed-v2): improve comment posting UX#4591

Merged
mergify[bot] merged 7 commits into
box:masterfrom
jackiejou:feat/activity-feed-v2-improvements
May 27, 2026
Merged

feat(activity-feed-v2): improve comment posting UX#4591
mergify[bot] merged 7 commits into
box:masterfrom
jackiejou:feat/activity-feed-v2-improvements

Conversation

@jackiejou
Copy link
Copy Markdown
Contributor

@jackiejou jackiejou commented May 26, 2026

Summary

Improves comment posting UX in the activity feed across V1 and V2.

V2 mention popover - Open the popover immediately on @ (parity with box-annotations and V1) and show V1-style copy:

  • "Mention someone to notify them" while the input is empty
  • "No users found" once the user types
  • No API round-trip on empty or whitespace-only queries

V1 + V2 trim - Strip leading and trailing whitespace from comment text before sending to the API. Inner whitespace is preserved.

V1 disable post button - The post button is now disabled when the editor is empty or whitespace-only, matching the form validation that already blocks submit. Visible feedback instead of a clickable button that does nothing.

V2 auto-scroll on post - After a successful post, scroll to the new item using a snapshot of feed item ids taken at post time, so a concurrent push from another user does not hijack the viewport. onCommentCreate is awaited so rejections are logged and do not arm the scroll for an unrelated update.

V1 mention selector is otherwise unchanged. The trim change to getFormattedCommentText is intentional and applies to all V1 comment surfaces.

Test plan

  • V2 sidebar: type @, popover opens with "Mention someone to notify them"; type a letter, popover shows matches or "No users found"
  • V2 sidebar: post a comment with surrounding whitespace, confirm the saved comment is trimmed
  • V2 sidebar: post a comment, confirm the feed scrolls to the new comment
  • V2 sidebar: with another tab posting, confirm the local user is scrolled only to their own post (not the stranger post)
  • V1 sidebar: open the comment form with an empty editor, confirm the Post button is visibly disabled
  • V1 sidebar: type only spaces, confirm Post stays disabled; type a real character, confirm it enables
  • V1 sidebar: post with surrounding whitespace, confirm the saved comment is trimmed
  • Unit tests: yarn test --watchAll=false --testPathPattern="(activity-feed-v2|comment-form|draft-js-mention-selector)" passes

Summary by CodeRabbit

  • New Features

    • Post button disables when comment input is empty.
    • Mention selector trims queries, supports empty queries, and shows improved empty-state messaging.
    • Editor container wrapped to apply editor-specific spacing.
  • Bug Fixes

    • Serialized comment text is trimmed to remove leading/trailing whitespace before posting.
  • Style

    • Added padding for editor and mention-empty activity feed states.
  • Tests

    • Expanded coverage for mentions, trimming, disabled post state, posting, and scroll-to-new-post behavior.

Review Change Stack

Mention popover (V2):
- Open the mention popover on @ before any character is typed
- Show the start prompt while empty and "No users found" once the user types
- Skip the API round-trip on empty or whitespace-only queries

Comment text (V1 + V2):
- Trim leading and trailing whitespace from comment text before sending
  to the API. Inner whitespace is preserved.

V1 post button:
- Disable the post button when the editor is empty or whitespace-only,
  matching the validation already enforced on submit.

V2 auto-scroll after posting:
- After a successful post, scroll to the new item using a snapshot of
  ids taken at post time, so a concurrent push from another user does
  not hijack the viewport.
- Await onCommentCreate so rejections are logged and do not arm the
  scroll for an unrelated update.
@jackiejou jackiejou requested review from a team as code owners May 26, 2026 23:34
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Whitespace trimming added to comment utilities and ActivityFeed helpers; post button disabled for empty editor content; ActivityFeedV2 trims mention queries, supports localized empty-state UI, wraps the editor with styling, switches posting to trimmed serialization, snapshots feed ids to scroll only to newly-created user posts, and expands tests.

Changes

Comment trimming and post button UX

Layer / File(s) Summary
Text trimming foundation
src/components/form-elements/draft-js-mention-selector/utils.js, src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js, src/elements/content-sidebar/activity-feed-v2/helpers.ts, src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts
getFormattedCommentText() and serializeEditorContent() now trim serialized text before returning; tests validate whitespace normalization while preserving hasMention.
Comment form controls and focus tests
src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js, src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js, src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js, src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js, src/elements/content-sidebar/activity-feed/comment/__tests__/*
CommentForm computes isPostDisabled from trimmed editor text and passes it to CommentFormControls; CommentFormControls accepts isDisabled to disable the Post button; focus-related tests updated to provide focus-return behavior.
Activity Feed V2 mention UI and styling
src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx, src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss
ActivityFeedV2 trims mention queries, enables allowEmptyQuery with a renderEmpty renderer showing localized “start mention” or “no users found”, wraps the editor in bcs-NewActivityFeed-editor, and adds new padding styles for editor and mention-empty states.
Activity Feed V2 posting, scroll-to-new-post, and tests
src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx, src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx
Posting uses serializeEditorContent and requires non-empty trimmed text; knownIdsBeforePostRef snapshots feed ids before post and an effect scrolls to the first newly-created user-authored item not in that snapshot; tests/mocks updated to capture editor props and validate mention popover, posting, and robust scroll behaviors.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Editor as ActivityFeedV2 Editor
  participant Query as Query Trim/Fetch
  participant Selector as User Selector
  participant Empty as renderEmpty UI
  User->>Editor: Focus mention input / type "@"
  Editor->>Query: Send query text
  Query->>Query: Trim query
  alt Query is empty or whitespace
    Query->>Empty: Render "start mention" state
    Empty-->>Selector: Show localized prompt
  else Query is non-empty
    Query->>Selector: Call getMentionAsync(trimmed)
    Selector->>Selector: Fetch and transform user results
    alt Results found
      Selector-->>Editor: Display user list
    else No results
      Selector->>Empty: Render "no users found" state
      Empty-->>Editor: Show localized message
    end
  end
Loading
sequenceDiagram
  participant User
  participant Editor as handleCommentPost
  participant Serialize as serializeEditorContent
  participant API as Comment API
  participant State as feedItems
  participant Scroll as scrollEffect
  User->>Editor: Click post
  Editor->>Serialize: Serialize editor content
  Serialize-->>Editor: Return trimmed text + hasMention
  alt Text is empty/whitespace
    Editor->>User: Skip post
  else Text is non-empty
    Editor->>API: Create comment
    Editor->>Editor: Snapshot knownIdsBeforePostRef
    API-->>State: feedItems update
    State->>Scroll: Detect new items
    Scroll->>Scroll: Find first new user-authored item
    Scroll->>Scroll: Call scrollTo(itemId)
    alt scrollTo succeeds
      Scroll-->>User: Scroll to new comment
    else scrollTo fails
      Scroll->>Scroll: Retry on next feedItems change
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

ready-to-merge

Suggested reviewers

  • ahorowitz123
  • kduncanhsu
  • reneshen0328

Poem

🐰 I nibble spaces, tabs, and stray newlines bright,
Mentions tidy, posts sleep till content's right,
New feeds scroll true to the user's own light,
Tests hop along to keep behavior tight,
Hooray — clean text, and the UI feels light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(activity-feed-v2): improve comment posting UX' directly summarizes the main change: enhancing comment posting user experience in the activity feed v2 component.
Description check ✅ Passed The PR description is comprehensive and complete, covering the summary, objectives for V2 and V1, the rationale for each change, and a detailed test plan with clear manual and automated test instructions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js (1)

173-187: ⚡ Quick win

Add an explicit whitespace-only test for post disablement.

Current coverage checks empty and non-whitespace text, but not " \n\t" input, which is the trim-dependent edge this change relies on.

Proposed test addition
 describe('post button disabled state', () => {
     const findControls = wrapper => wrapper.find('CommentInputControls');

     test('should pass isDisabled=true to the controls when the editor is empty', () => {
         const wrapper = getWrapper({ isOpen: true });

         expect(findControls(wrapper).prop('isDisabled')).toBe(true);
     });

+    test('should pass isDisabled=true to the controls when the editor has only whitespace', () => {
+        const wrapper = getWrapper({ isOpen: true, tagged_message: '   \n\t   ' });
+
+        expect(findControls(wrapper).prop('isDisabled')).toBe(true);
+    });
+
     test('should pass isDisabled=false to the controls when the editor has non-whitespace content', () => {
         const wrapper = getWrapper({ isOpen: true, tagged_message: 'hello' });

         expect(findControls(wrapper).prop('isDisabled')).toBe(false);
     });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`
around lines 173 - 187, The tests for post button disabled state are missing a
case for whitespace-only editor content; add a test inside the existing "post
button disabled state" describe block that uses getWrapper({ isOpen: true,
tagged_message: '   \n\t' }) and asserts
findControls(wrapper).prop('isDisabled') is true so the CommentInputControls
receives isDisabled=true for trim-only input. Use the same helper findControls
and test naming style as the other two tests to keep consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx`:
- Around line 265-271: The current logic uses filteredItems.find(item =>
!knownIds.has(item.id)) which can pick a concurrent insert; instead locate the
exact pending post before scrolling: use the pending-post identity (e.g. a
pendingPostId or pendingPostRef that tracks the just-submitted comment) to find
the matching item in filteredItems (e.g. const newItem = filteredItems.find(item
=> item.id === pendingPostIdRef.current)); only call
scrollHandle.scrollTo(newItem.id) if that exact match exists, and then clear
knownIdsBeforePostRef.current as before; if no pending-post id is available, do
not fall back to “first unknown id.”

---

Nitpick comments:
In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`:
- Around line 173-187: The tests for post button disabled state are missing a
case for whitespace-only editor content; add a test inside the existing "post
button disabled state" describe block that uses getWrapper({ isOpen: true,
tagged_message: '   \n\t' }) and asserts
findControls(wrapper).prop('isDisabled') is true so the CommentInputControls
receives isDisabled=true for trim-only input. Use the same helper findControls
and test naming style as the other two tests to keep consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77e3b531-7d29-432c-b60f-b74dd432b757

📥 Commits

Reviewing files that changed from the base of the PR and between 49bffba and 7d400fc.

📒 Files selected for processing (11)
  • src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js
  • src/components/form-elements/draft-js-mention-selector/utils.js
  • src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss
  • src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx
  • src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx
  • src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts
  • src/elements/content-sidebar/activity-feed-v2/helpers.ts
  • src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js
  • src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js
  • src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js
  • src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js

Comment thread src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx
commentEditorState is initialized in the constructor and is never
undefined in production. The optional chain was a workaround for a
test that mocked EditorState.moveFocusToEnd globally and leaked the
mock across tests. Restore the original after each test instead.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js (1)

185-199: ⚡ Quick win

Add an explicit whitespace-only disabled test case.

This suite should also assert isDisabled=true for tagged_message like ' \n\t ' to directly lock the trim-based behavior promised by this PR.

✅ Suggested test addition
 describe('post button disabled state', () => {
     const findControls = wrapper => wrapper.find('CommentInputControls');

     test('should pass isDisabled=true to the controls when the editor is empty', () => {
         const wrapper = getWrapper({ isOpen: true });

         expect(findControls(wrapper).prop('isDisabled')).toBe(true);
     });

+    test('should pass isDisabled=true to the controls when the editor has only whitespace', () => {
+        const wrapper = getWrapper({ isOpen: true, tagged_message: '   \n\t  ' });
+
+        expect(findControls(wrapper).prop('isDisabled')).toBe(true);
+    });
+
     test('should pass isDisabled=false to the controls when the editor has non-whitespace content', () => {
         const wrapper = getWrapper({ isOpen: true, tagged_message: 'hello' });

         expect(findControls(wrapper).prop('isDisabled')).toBe(false);
     });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`
around lines 185 - 199, Add a new test in the "post button disabled state" suite
that uses getWrapper({ isOpen: true, tagged_message: '   \n\t  ' }) and asserts
findControls(wrapper).prop('isDisabled') === true to cover the whitespace-only
case; place it alongside the existing tests referencing CommentInputControls,
findControls, getWrapper, and the isDisabled prop so the trim-based behavior is
explicitly validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`:
- Around line 185-199: Add a new test in the "post button disabled state" suite
that uses getWrapper({ isOpen: true, tagged_message: '   \n\t  ' }) and asserts
findControls(wrapper).prop('isDisabled') === true to cover the whitespace-only
case; place it alongside the existing tests referencing CommentInputControls,
findControls, getWrapper, and the isDisabled prop so the trim-based behavior is
explicitly validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0c57716-fcb5-493e-89b4-9e2fbdc03e20

📥 Commits

Reviewing files that changed from the base of the PR and between 7d400fc and d485c37.

📒 Files selected for processing (2)
  • src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js
  • src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js

jackiejou added 4 commits May 26, 2026 17:26
The existing tests overwrote EditorState.moveFocusToEnd with a bare
jest.fn() that returns undefined. Once CommentForm reads the editor
state during render to compute isPostDisabled, the undefined return
crashed the component. Make the mock return its input so the editor
state stays valid while the spy still tracks calls.
The scroll-after-post effect picked the first item missing from the
pre-post snapshot, which could match a concurrent push from another
user that landed in the same render. Filter the candidate to comments
or annotations whose author id matches currentUserId so a teammate
post does not hijack the viewport.

Also adds a whitespace-only post button disabled test in V1 to lock
in the trim-based behavior.
The list rows are inset via padding on each li, but the editor was a
sibling of the list with no equivalent inset, so it stretched edge to
edge while rows did not. Wrap the editor in a sibling div with the
same horizontal padding so the two surfaces line up.
The previous commit added padding to our wrapper, which compounded
with the vendor editor wrapper padding instead of replacing it.
Target the vendor wrapper directly via the BUIE wrapper child so the
editor outline lines up with the list rows.
@mergify mergify Bot added the queued label May 27, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented May 27, 2026

Merge Queue Status

  • Entered queue2026-05-27 21:20 UTC · Rule: Automatic strict merge
  • Checks passed · in-place
  • Merged2026-05-27 21:31 UTC · at 2b1be0e9f52bcd18c9eb2af9beb65dd262ba7412 · squash

This pull request spent 10 minutes 40 seconds in the queue, including 9 minutes 58 seconds running CI.

Required conditions to merge
  • #approved-reviews-by >= 1 [🛡 GitHub branch protection]
  • #review-threads-unresolved = 0 [🛡 GitHub branch protection]
  • github-review-decision = APPROVED [🛡 GitHub branch protection]
  • any of [🛡 GitHub branch protection]:
    • check-success = Summary
    • check-neutral = Summary
    • check-skipped = Summary
  • any of [🛡 GitHub branch protection]:
    • check-success = lint_test_build
    • check-neutral = lint_test_build
    • check-skipped = lint_test_build
  • any of [🛡 GitHub branch protection]:
    • check-success = license/cla
    • check-neutral = license/cla
    • check-skipped = license/cla
  • any of [🛡 GitHub branch protection]:
    • check-success = lint_pull_request
    • check-neutral = lint_pull_request
    • check-skipped = lint_pull_request

@mergify mergify Bot merged commit 1a0e73e into box:master May 27, 2026
9 of 10 checks passed
@mergify mergify Bot removed the queued label May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants