feat(activity-feed-v2): improve comment posting UX#4591
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughWhitespace 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. ChangesComment trimming and post button UX
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js (1)
173-187: ⚡ Quick winAdd 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
📒 Files selected for processing (11)
src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.jssrc/components/form-elements/draft-js-mention-selector/utils.jssrc/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scsssrc/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsxsrc/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsxsrc/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.tssrc/elements/content-sidebar/activity-feed-v2/helpers.tssrc/elements/content-sidebar/activity-feed/comment-form/CommentForm.jssrc/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.jssrc/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.jssrc/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js
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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js (1)
185-199: ⚡ Quick winAdd an explicit whitespace-only disabled test case.
This suite should also assert
isDisabled=truefortagged_messagelike' \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
📒 Files selected for processing (2)
src/elements/content-sidebar/activity-feed/comment-form/CommentForm.jssrc/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js
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.
Merge Queue Status
This pull request spent 10 minutes 40 seconds in the queue, including 9 minutes 58 seconds running CI. Required conditions to merge
|
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: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.
onCommentCreateis 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
getFormattedCommentTextis intentional and applies to all V1 comment surfaces.Test plan
@, popover opens with "Mention someone to notify them"; type a letter, popover shows matches or "No users found"yarn test --watchAll=false --testPathPattern="(activity-feed-v2|comment-form|draft-js-mention-selector)"passesSummary by CodeRabbit
New Features
Bug Fixes
Style
Tests