Skip to content

Migrate comment moderation filters to new filter abstraction#27043

Merged
kevinansfield merged 9 commits into
mainfrom
ber-3505-comment-filters-migration
May 6, 2026
Merged

Migrate comment moderation filters to new filter abstraction#27043
kevinansfield merged 9 commits into
mainfrom
ber-3505-comment-filters-migration

Conversation

@jonatansberg
Copy link
Copy Markdown
Member

What changed

This reworks comment moderation filters in apps/posts to use the same canonical field/codec/NQL pipeline as members instead of the old ad-hoc query-param implementation.

It also keeps id and thread out of filter state:

  • filter is now the only concern of the comment filter-state hook
  • legacy ?id=is:<commentId> deep links are handled separately at the page level
  • single-comment mode still clears back to the full comments view

Follow-up work on this branch also tightened the shared value-source behavior so selected author/post filters remain representable when the backing record cannot be hydrated, without allowing synthetic ID: ... placeholders to win over real hydrated labels in the combined post/page source.

Why

The previous comments filter implementation had a hand-rolled buildNqlFilter() path, custom URL shape, no NQL parsing, and browser-local date handling. That had drifted away from the mainline filter abstractions and made comments harder to reason about and maintain.

This brings comments back onto the current shared model while keeping the deep-link behavior that moderation flows still rely on.

User impact

  • comment moderation filters now round-trip through canonical ?filter=<NQL> URLs
  • filter parsing/serialization is aligned with members
  • exact-date comment filtering uses site timezone semantics instead of browser-local assumptions
  • legacy single-comment links via ?id=is:<commentId> still work
  • missing selected author/post resources stay visible in filter UI instead of collapsing into a broken state

Validation

  • cd apps/posts && yarn vitest run test/unit/hooks/create-remote-value-source.test.tsx test/unit/hooks/create-combined-value-source.test.tsx test/unit/views/comments test/unit/utils/filter-normalization.test.ts
  • cd apps/posts && yarn eslint --ext .ts,.tsx src/hooks/filter-sources/create-combined-value-source.ts src/hooks/filter-sources/use-post-resource-value-source.ts test/unit/hooks/create-combined-value-source.test.tsx
  • Earlier targeted verification on the main migration also included:
    • cd apps/posts && yarn tsc --noEmit
    • focused lint/test runs for the canonical comments filter modules

Notes

  • existing moderation link producers remain on ?id=is:<commentId> in this pass
  • .claude/worktrees/ is an unrelated untracked local directory and is not part of this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 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

This pull request adds comprehensive comment filtering infrastructure to the posts application. Changes include new comment field definitions with custom codecs, filter parsing and serialization functions supporting timezone-aware date filtering, and refactored filter state management using a canonical filter query parameter. The comments view is refactored to support single-comment viewing via URL parameters. Value source implementations are extended to synthesize missing options for unresolved selected values. Shared filter utilities are introduced for date normalization, operator label creation, and field-type-based discovery. The member-fields configuration is updated to use centralized date filtering. The temporal-polyfill dependency is added for timezone support. Extensive unit and component tests cover new functionality.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: migrating comment moderation filters to use the new shared filter abstraction, which is the primary focus of this changeset.
Description check ✅ Passed The PR description is directly related to the changeset, providing context on the migration from ad-hoc query params to canonical field/codec/NQL pipeline, explaining the rationale and user impact.
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
  • Commit unit tests in branch ber-3505-comment-filters-migration

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@codeCraft-Ritik codeCraft-Ritik left a comment

Choose a reason for hiding this comment

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

Excellent refactor! Bringing comment moderation filters in line with the canonical pipeline is a big improvement

@jonatansberg jonatansberg changed the title 🎨 Migrate comment moderation filters to canonical query state Migrate comment moderation filters to canonical query state Apr 1, 2026
@jonatansberg jonatansberg changed the title Migrate comment moderation filters to canonical query state Migrate comment moderation filters to new filter abstraction Apr 1, 2026
@jonatansberg jonatansberg force-pushed the ber-3505-comment-filters-migration branch from 9e54b66 to 4b9b6d0 Compare April 1, 2026 13:49
@jonatansberg jonatansberg marked this pull request as ready for review April 2, 2026 11:33
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: 3

🧹 Nitpick comments (3)
apps/posts/test/unit/utils/filter-normalization.test.ts (1)

5-11: Consider using String.raw for improved readability.

Per SonarCloud, the heavily escaped string literals can be more readable using String.raw:

💡 Optional refactor for readability
     it('escapes single quotes for NQL strings', () => {
-        expect(escapeNqlString('can\'t stop')).toBe('\'can\\\'t stop\'');
+        expect(escapeNqlString("can't stop")).toBe(String.raw`'can\'t stop'`);
     });

     it('escapes backslashes before single quotes for NQL strings', () => {
-        expect(escapeNqlString('test\\\'value')).toBe('\'test\\\\\\\'value\'');
+        expect(escapeNqlString(String.raw`test\'value`)).toBe(String.raw`'test\\\'value'`);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/test/unit/utils/filter-normalization.test.ts` around lines 5 - 11,
The test assertions use heavily escaped literal strings which reduce
readability; update the two expectations in the escapeNqlString unit tests to
use String.raw template literals instead of manually escaping backslashes and
quotes so the expected strings are clearer while keeping the same runtime value.
Locate the tests referencing escapeNqlString (the two it blocks shown) and
replace the right-hand expected string literals with equivalent String.raw``
templates that include the surrounding NQL single quotes and the needed
backslashes, verifying the tests still assert the same string values.
apps/posts/test/unit/views/comments/use-filter-state.test.tsx (1)

74-75: Prefer order-insensitive query assertions.

These expectations are semantically about params, not string order. Consider asserting via URLSearchParams lookups to avoid brittle failures from serialization ordering.

✅ More robust assertion pattern
-            expect(result.current.query).toBe('thread=is%3Acomment_123&filter=count.reports%3A%3E0');
+            const params = new URLSearchParams(result.current.query);
+            expect(params.get('thread')).toBe('is:comment_123');
+            expect(params.get('filter')).toBe('count.reports:>0');
-            expect(result.current.query).toBe('id=is%3Acomment_123&thread=is%3Acomment_456');
+            const params = new URLSearchParams(result.current.query);
+            expect(params.get('id')).toBe('is:comment_123');
+            expect(params.get('thread')).toBe('is:comment_456');
+            expect(params.has('filter')).toBe(false);

Also applies to: 92-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/test/unit/views/comments/use-filter-state.test.tsx` around lines
74 - 75, The test currently asserts the full serialized query string
(expect(result.current.query).toBe(...)) which is order-sensitive; change both
assertions (the one using result.current.query at the shown location and the
similar assertion around line 92) to parse result.current.query with
URLSearchParams and assert individual params and their values (e.g., use new
URLSearchParams(result.current.query).get('thread') and .get('filter') or check
has()/getAll() as appropriate) so the test validates param semantics instead of
serialization order.
apps/posts/src/hooks/filter-sources/create-combined-value-source.ts (1)

15-27: Extract fallback-option generation out of useOptions.

This is the nested branch SonarCloud is flagging. Pulling it into a tiny helper keeps the hook easier to scan and should clear the depth rule without changing behavior.

Refactor sketch
+function getMissingSelectedOptions<T>(
+    selectedValues: T[],
+    mergedOptions: FilterOption<T>[],
+    getMissingSelectedOption?: (selectedValue: T) => FilterOption<T>
+): FilterOption<T>[] {
+    if (!getMissingSelectedOption) {
+        return [];
+    }
+
+    return selectedValues.flatMap((selectedValue) => {
+        return mergedOptions.some(option => option.value === selectedValue)
+            ? []
+            : [getMissingSelectedOption(selectedValue)];
+    });
+}
+
 export function createCombinedValueSource<T = string>(
     useFirstSource: ValueSourceHook<T>,
     useSecondSource: ValueSourceHook<T>,
     getMissingSelectedOption?: (selectedValue: T) => FilterOption<T>
 ): ValueSourceHook<T> {
@@
         const useOptions = useCallback(({query, selectedValues}: ValueSourceParams<T>): ValueSourceState<T> => {
             const firstState = firstSource.useOptions({query, selectedValues});
             const secondState = secondSource.useOptions({query, selectedValues});
             const mergedOptions = mergeFilterOptions(firstState.options, secondState.options);
-            const fallbackOptions = getMissingSelectedOption ? selectedValues.flatMap((selectedValue) => {
-                const hasMatch = mergedOptions.some(option => option.value === selectedValue);
-
-                if (hasMatch) {
-                    return [];
-                }
-
-                return [getMissingSelectedOption(selectedValue)];
-            }) : [];
+            const fallbackOptions = getMissingSelectedOptions(
+                selectedValues,
+                mergedOptions,
+                getMissingSelectedOption
+            );
 
             return {
                 options: mergeFilterOptions(mergedOptions, fallbackOptions),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/src/hooks/filter-sources/create-combined-value-source.ts` around
lines 15 - 27, The nested branch that builds fallbackOptions inside useOptions
should be extracted into a small helper to reduce cognitive depth: create a
helper (e.g., buildFallbackOptions or getFallbackOptions) that accepts
(selectedValues, mergedOptions, getMissingSelectedOption) and returns the same
array of missing selected options, then replace the inline logic in useOptions
with a call to that helper; ensure you reference and use mergedOptions (from
mergeFilterOptions(firstState.options, secondState.options)), selectedValues,
and getMissingSelectedOption so behavior of useOptions, firstSource.useOptions,
and secondSource.useOptions remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/posts/src/hooks/filter-sources/create-remote-value-source.ts`:
- Around line 92-108: The nested selection/fallback logic inside the useMemo
that builds fallbackOptions should be extracted into a small helper to satisfy
the quality gate: create a pure function (e.g. buildFallbackOptions or
getFallbackOptions) that accepts getMissingSelectedOption, selectedValues and
mergedOptions and returns the computed array (iterate selectedValues, check
mergedOptions some match, call getMissingSelectedOption when missing); then
replace the inline nested logic in fallbackOptions with a call to that helper
inside useMemo and keep the dependency array as [mergedOptions, selectedValues,
getMissingSelectedOption].

In `@apps/posts/src/views/comments/comments.tsx`:
- Around line 22-27: The CommentsPage no longer forwards the legacy "thread"
scope to browse requests because useFilterState only returns filters now; update
CommentsPage to read the thread identifier from the URL (via useSearchParams) or
from singleCommentId and pass it into useBrowseComments so browse requests
receive {filter, thread} (or include thread inside the filters object) instead
of only {filter}. Concretely, locate the CommentsPage component and where
useBrowseComments is called, derive thread = searchParams.get('thread') ||
singleCommentId (or similar) and supply that thread into the hook call (and into
any places that build browse params at the other noted spots around the
component). Ensure setSearchParams behavior stays compatible (preserve thread
when updating filters) and update all instances referenced (the initial use of
useFilterState, the browse invocation, and the other blocks around the 54-56 and
133-155 ranges) to propagate thread consistently.

In `@apps/posts/test/unit/views/comments/comments.test.tsx`:
- Around line 123-129: Add an assertion that the thread browse param is
forwarded to useBrowseComments: mock or spy on useBrowseComments (the hook
imported/used by renderComments) and after calling
renderComments('/?thread=is:comment_456') and firing the filter update, assert
the mocked useBrowseComments was called with a browse params object that
includes thread: 'is:comment_456' (in addition to the existing expect on
'location-search'); this ensures renderComments/useBrowseComments receives the
thread param even after filter updates.

---

Nitpick comments:
In `@apps/posts/src/hooks/filter-sources/create-combined-value-source.ts`:
- Around line 15-27: The nested branch that builds fallbackOptions inside
useOptions should be extracted into a small helper to reduce cognitive depth:
create a helper (e.g., buildFallbackOptions or getFallbackOptions) that accepts
(selectedValues, mergedOptions, getMissingSelectedOption) and returns the same
array of missing selected options, then replace the inline logic in useOptions
with a call to that helper; ensure you reference and use mergedOptions (from
mergeFilterOptions(firstState.options, secondState.options)), selectedValues,
and getMissingSelectedOption so behavior of useOptions, firstSource.useOptions,
and secondSource.useOptions remains unchanged.

In `@apps/posts/test/unit/utils/filter-normalization.test.ts`:
- Around line 5-11: The test assertions use heavily escaped literal strings
which reduce readability; update the two expectations in the escapeNqlString
unit tests to use String.raw template literals instead of manually escaping
backslashes and quotes so the expected strings are clearer while keeping the
same runtime value. Locate the tests referencing escapeNqlString (the two it
blocks shown) and replace the right-hand expected string literals with
equivalent String.raw`` templates that include the surrounding NQL single quotes
and the needed backslashes, verifying the tests still assert the same string
values.

In `@apps/posts/test/unit/views/comments/use-filter-state.test.tsx`:
- Around line 74-75: The test currently asserts the full serialized query string
(expect(result.current.query).toBe(...)) which is order-sensitive; change both
assertions (the one using result.current.query at the shown location and the
similar assertion around line 92) to parse result.current.query with
URLSearchParams and assert individual params and their values (e.g., use new
URLSearchParams(result.current.query).get('thread') and .get('filter') or check
has()/getAll() as appropriate) so the test validates param semantics instead of
serialization order.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: db6e8c5f-5e2b-4d14-be3c-202fafcd4fd0

📥 Commits

Reviewing files that changed from the base of the PR and between fa6bd08 and 4b9b6d0.

📒 Files selected for processing (25)
  • apps/posts/src/hooks/filter-sources/create-combined-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-remote-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-member-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts
  • apps/posts/src/views/comments/comment-fields.ts
  • apps/posts/src/views/comments/comment-filter-query.ts
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comments-filters.tsx
  • apps/posts/src/views/comments/hooks/use-filter-state.ts
  • apps/posts/src/views/comments/use-comment-filter-fields.ts
  • apps/posts/src/views/filters/filter-normalization.test.ts
  • apps/posts/src/views/filters/filter-normalization.ts
  • apps/posts/src/views/filters/filter-operator-options.ts
  • apps/posts/src/views/members/member-fields.ts
  • apps/posts/src/views/members/use-member-filter-fields.ts
  • apps/posts/test/unit/hooks/create-combined-value-source.test.tsx
  • apps/posts/test/unit/hooks/create-remote-value-source.test.tsx
  • apps/posts/test/unit/utils/filter-normalization.test.ts
  • apps/posts/test/unit/views/comments/comment-fields.test.ts
  • apps/posts/test/unit/views/comments/comment-filter-query.test.ts
  • apps/posts/test/unit/views/comments/comments.test.tsx
  • apps/posts/test/unit/views/comments/use-filter-state.test.tsx
  • apps/posts/vite.config.mjs
  • apps/posts/vitest.config.ts
💤 Files with no reviewable changes (2)
  • apps/posts/src/views/filters/filter-normalization.test.ts
  • apps/posts/vitest.config.ts

Comment thread apps/posts/src/hooks/filter-sources/create-remote-value-source.ts
Comment thread apps/posts/src/views/comments/comments.tsx
Comment thread apps/posts/test/unit/views/comments/comments.test.tsx Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b9b6d0002

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +35 to +37
if (singleCommentId) {
return `id:${escapeNqlString(singleCommentId)}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop ignoring newly added filters in single-comment mode

When id=is:<comment> is present, this branch always forces effectiveFilter to id:'...' and drops nql, so row-level quick filters (author/post) update URL state but never affect the API query while single-comment mode is active. In practice, moderators can click filter affordances in the comment row and get no result change until they manually leave single mode, which is a behavior regression introduced by separating id from filter state.

Useful? React with 👍 / 👎.

jonatansberg added a commit that referenced this pull request Apr 2, 2026
ref #27043
Preserves thread-scoped browsing and exits single-comment mode when row filters are applied
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 2, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7c9a3d500

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const {data: settingsData, isLoading: isSettingsLoading} = useBrowseSettings({});
const singleCommentId = useMemo(() => getSingleCommentIdParam(searchParams), [searchParams]);
const filterParam = searchParams.get('filter') ?? undefined;
const shouldDelayHydration = !singleCommentId && shouldDelayCommentDateFilterHydration(filterParam, Boolean(settingsData), isSettingsLoading);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Block date filtering until timezone is loaded

This hydration gate only waits when a filter query param already exists, so the page can render with fallback UTC while settings are still loading. In that window, adding a created_at filter serializes UTC day bounds; after settings resolve, parseCommentFilter no longer matches those bounds for the real site timezone and drops the predicate, so the date filter silently disappears and the API query is cleared. This is a user-visible race on first load for slow settings responses.

Useful? React with 👍 / 👎.

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/posts/src/views/comments/comments.tsx`:
- Around line 74-81: The reset key for CommentsList only uses effectiveFilter
but must include the thread identifier too; update the logic that computes
resetKey (the variable named resetKey near the CommentsList usage) to
incorporate threadParam along with effectiveFilter (e.g. combine threadParam and
effectiveFilter into a single string or tuple) so the list resets when either
the thread or filter changes; apply the same change to the other occurrence
referenced around line 137 where resetKey is computed/used.
- Around line 61-63: handleShowAllComments currently clears the entire query
string which loses the thread scope; modify it to only remove the single-comment
identifier instead of replacing all params by creating a copy of the existing
URLSearchParams, deleting the 'id' (or whatever single-comment param is used)
while leaving 'thread' and other params intact, and then calling setSearchParams
with that modified params object (keeping {replace: false}) so exiting
single-comment mode preserves thread context.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8e8ed441-ed5a-4da0-a4dd-4d906ff68588

📥 Commits

Reviewing files that changed from the base of the PR and between 4b9b6d0 and b7c9a3d.

📒 Files selected for processing (3)
  • apps/posts/src/hooks/filter-sources/create-remote-value-source.ts
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/test/unit/views/comments/comments.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/posts/test/unit/views/comments/comments.test.tsx

Comment on lines +61 to +63
const handleShowAllComments = useCallback(() => {
setSearchParams(new URLSearchParams(), {replace: false});
}, [setSearchParams]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Preserve the current thread scope when leaving single-comment mode.

Line 62 clears the whole query string, so a URL like ?thread=...&id=... loses its thread context here even though the row-filter path above preserves it. If this action is only meant to exit single-comment mode, remove the relevant params instead of replacing everything.

Suggested fix
     const handleShowAllComments = useCallback(() => {
-        setSearchParams(new URLSearchParams(), {replace: false});
-    }, [setSearchParams]);
+        const nextSearchParams = new URLSearchParams(searchParams);
+        nextSearchParams.delete('id');
+        nextSearchParams.delete('filter');
+        setSearchParams(nextSearchParams, {replace: false});
+    }, [searchParams, setSearchParams]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleShowAllComments = useCallback(() => {
setSearchParams(new URLSearchParams(), {replace: false});
}, [setSearchParams]);
const handleShowAllComments = useCallback(() => {
const nextSearchParams = new URLSearchParams(searchParams);
nextSearchParams.delete('id');
nextSearchParams.delete('filter');
setSearchParams(nextSearchParams, {replace: false});
}, [searchParams, setSearchParams]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/src/views/comments/comments.tsx` around lines 61 - 63,
handleShowAllComments currently clears the entire query string which loses the
thread scope; modify it to only remove the single-comment identifier instead of
replacing all params by creating a copy of the existing URLSearchParams,
deleting the 'id' (or whatever single-comment param is used) while leaving
'thread' and other params intact, and then calling setSearchParams with that
modified params object (keeping {replace: false}) so exiting single-comment mode
preserves thread context.

Comment on lines +74 to +81
searchParams: {
...(threadParam && !singleCommentId ? {thread: threadParam} : {}),
...(effectiveFilter ? {filter: effectiveFilter} : {})
},
keepPreviousData: true
});
// If we are fetching comments, but not fetching the next page and not refetching, we should show the loading indicator
const shouldShowLoading = isFetching && !isFetchingNextPage && !isRefetching;
const resetKey = effectiveFilter ?? '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Include thread in the list reset key.

The browse query changes with both thread and filter here, but Line 81 only keys CommentsList by effectiveFilter. Switching between thread-scoped URLs with the same filter leaves resetKey unchanged, so any list state tied to that prop will carry across to the wrong dataset.

Suggested fix
-    const resetKey = effectiveFilter ?? '';
+    const resetKey = singleCommentId
+        ? (effectiveFilter ?? '')
+        : `${threadParam ?? ''}|${effectiveFilter ?? ''}`;

Also applies to: 137-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/src/views/comments/comments.tsx` around lines 74 - 81, The reset
key for CommentsList only uses effectiveFilter but must include the thread
identifier too; update the logic that computes resetKey (the variable named
resetKey near the CommentsList usage) to incorporate threadParam along with
effectiveFilter (e.g. combine threadParam and effectiveFilter into a single
string or tuple) so the list resets when either the thread or filter changes;
apply the same change to the other occurrence referenced around line 137 where
resetKey is computed/used.

jonatansberg added a commit that referenced this pull request Apr 29, 2026
ref #27043
Preserves thread-scoped browsing and exits single-comment mode when row filters are applied
@jonatansberg jonatansberg force-pushed the ber-3505-comment-filters-migration branch 2 times, most recently from c82ec7d to 7b007f8 Compare April 29, 2026 07:48
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/hooks/pre-commit (1)

40-53: ⚠️ Potential issue | 🟡 Minor

Use staged-index check instead of working-tree check for empty-commit detection.

Line 51 uses git status --porcelain, which includes unstaged changes. If staged changes are empty but unstaged files exist, this block won’t catch the “nothing staged to commit” case.

Proposed fix
-    if output=$(git status --porcelain) && [ -z "$output" ]; then
+    if git diff --cached --quiet --exit-code; then
         echo -e "nothing to commit, working tree clean"
         exit 1
     fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/hooks/pre-commit around lines 40 - 53, The pre-commit hook currently
checks the working tree with `git status --porcelain` after removing
MOD_SUBMODULES, which misses the case where there are unstaged changes but no
staged changes; change the empty-commit detection to inspect the staged index
instead (use git diff --cached --name-only or git diff --cached --quiet) and if
the staged diff is empty print the "nothing to commit, working tree clean"
message and exit 1; update the block that follows MOD_SUBMODULES handling (the
if that assigns output and checks -z) to use the staged-index check and preserve
the same message and exit behavior.
🧹 Nitpick comments (1)
apps/posts/src/hooks/filter-sources/create-combined-value-source.ts (1)

19-27: Consider reusing the fallback builder to avoid logic drift.

Line 19 duplicates the same missing-selected synthesis now in create-remote-value-source.ts. A shared helper would reduce divergence risk between remote and combined value-source behavior.

♻️ Suggested direction
-const fallbackOptions = getMissingSelectedOption ? selectedValues.flatMap((selectedValue) => {
-    const hasMatch = mergedOptions.some(option => option.value === selectedValue);
-
-    if (hasMatch) {
-        return [];
-    }
-
-    return [getMissingSelectedOption(selectedValue)];
-}) : [];
+const fallbackOptions = buildFallbackOptions(
+    selectedValues,
+    mergedOptions,
+    getMissingSelectedOption
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/src/hooks/filter-sources/create-combined-value-source.ts` around
lines 19 - 27, This duplicate missing-selected synthesis logic (the
getMissingSelectedOption usage that builds fallbackOptions from selectedValues
and mergedOptions) should be extracted into a shared helper and reused to avoid
drift; create or import a function like
buildMissingSelectedFallback(selectedValues, mergedOptions,
getMissingSelectedOption) (or reuse the helper from
create-remote-value-source.ts), replace the inline flatMap in
create-combined-value-source.ts with a call to that helper, and update
create-remote-value-source.ts to call the same helper so both code paths share
the identical logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In @.github/hooks/pre-commit:
- Around line 40-53: The pre-commit hook currently checks the working tree with
`git status --porcelain` after removing MOD_SUBMODULES, which misses the case
where there are unstaged changes but no staged changes; change the empty-commit
detection to inspect the staged index instead (use git diff --cached --name-only
or git diff --cached --quiet) and if the staged diff is empty print the "nothing
to commit, working tree clean" message and exit 1; update the block that follows
MOD_SUBMODULES handling (the if that assigns output and checks -z) to use the
staged-index check and preserve the same message and exit behavior.

---

Nitpick comments:
In `@apps/posts/src/hooks/filter-sources/create-combined-value-source.ts`:
- Around line 19-27: This duplicate missing-selected synthesis logic (the
getMissingSelectedOption usage that builds fallbackOptions from selectedValues
and mergedOptions) should be extracted into a shared helper and reused to avoid
drift; create or import a function like
buildMissingSelectedFallback(selectedValues, mergedOptions,
getMissingSelectedOption) (or reuse the helper from
create-remote-value-source.ts), replace the inline flatMap in
create-combined-value-source.ts with a call to that helper, and update
create-remote-value-source.ts to call the same helper so both code paths share
the identical logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c252334d-2786-481f-958e-065fbe58125e

📥 Commits

Reviewing files that changed from the base of the PR and between b7c9a3d and c82ec7d.

📒 Files selected for processing (7)
  • .github/hooks/pre-commit
  • .lintstagedrc.cjs
  • apps/posts/src/hooks/filter-sources/create-combined-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-remote-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-member-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/posts/src/hooks/filter-sources/use-member-value-source.ts
  • apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts
  • apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts

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.

♻️ Duplicate comments (1)
apps/posts/src/views/comments/comments.tsx (1)

29-29: ⚠️ Potential issue | 🟠 Major

Thread-scoped deep links are not being propagated to browse params.

Line 75 only forwards filter, and Line 81 keys resets only by effectiveFilter. If ?thread=is:... is intended to scope browsing, this drops scope at fetch time and can preserve stale list state across thread switches.

Suggested fix
 const CommentsPage: React.FC<{timezone: string; singleCommentId?: string}> = ({
     timezone,
     singleCommentId
 }) => {
     const [searchParams, setSearchParams] = useSearchParams();
+    const threadParam = searchParams.get('thread') ?? undefined;
     const {filters, nql, setFilters} = useFilterState(timezone);
@@
     } = useBrowseComments({
         searchParams: {
+            ...(!singleCommentId && threadParam ? {thread: threadParam} : {}),
             ...(effectiveFilter ? {filter: effectiveFilter} : {})
         },
         keepPreviousData: true
     });
@@
-    const resetKey = effectiveFilter ?? '';
+    const resetKey = singleCommentId
+        ? (effectiveFilter ?? '')
+        : `${threadParam ?? ''}|${effectiveFilter ?? ''}`;
#!/bin/bash
# Verify whether thread-scoped browse is expected and whether it's forwarded from this page.

# 1) Inspect call sites and params passed to useBrowseComments
rg -nP --type=ts -C3 '\buseBrowseComments\s*\('

# 2) Inspect this file for thread extraction/forwarding
rg -nP --type=ts -C3 '\bthread\b' apps/posts/src/views/comments/comments.tsx

# 3) Inspect available type/contracts around browse search params for comments
rg -nP --type=ts -C4 'searchParams\s*:\s*\{|thread\??\s*:' 

Also applies to: 74-82

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/src/views/comments/comments.tsx` at line 29, The browse call is
only forwarding `filter` and the reset key is based only on `effectiveFilter`,
dropping any `thread` scope from `useSearchParams`; update the forwarding and
reset logic to include the thread-scoped param so thread-scoped deep links
persist. Concretely, read the thread value from `useSearchParams()` (e.g. `const
thread = searchParams.get('thread')`) and include it in the params passed into
`useBrowseComments(...)` and in the reset/cache key (alongside
`effectiveFilter`), and ensure any `setSearchParams` logic preserves or clears
the `thread` param consistently when changing filters. Reference symbols:
useSearchParams, searchParams, useBrowseComments, filter, effectiveFilter,
setSearchParams.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/posts/src/views/comments/comments.tsx`:
- Line 29: The browse call is only forwarding `filter` and the reset key is
based only on `effectiveFilter`, dropping any `thread` scope from
`useSearchParams`; update the forwarding and reset logic to include the
thread-scoped param so thread-scoped deep links persist. Concretely, read the
thread value from `useSearchParams()` (e.g. `const thread =
searchParams.get('thread')`) and include it in the params passed into
`useBrowseComments(...)` and in the reset/cache key (alongside
`effectiveFilter`), and ensure any `setSearchParams` logic preserves or clears
the `thread` param consistently when changing filters. Reference symbols:
useSearchParams, searchParams, useBrowseComments, filter, effectiveFilter,
setSearchParams.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f7fb471-d5bf-4df7-83c1-82e68c8fc5e0

📥 Commits

Reviewing files that changed from the base of the PR and between c82ec7d and 7b007f8.

📒 Files selected for processing (8)
  • apps/posts/src/views/comments/comments.tsx
  • apps/posts/src/views/comments/components/comments-filters.tsx
  • apps/posts/src/views/comments/hooks/use-filter-state.ts
  • apps/posts/src/views/comments/legacy-comment-filter-query.ts
  • apps/posts/src/views/comments/use-comment-filter-fields.ts
  • apps/posts/test/unit/views/comments/comments.test.tsx
  • apps/posts/test/unit/views/comments/use-comment-filter-fields.test.tsx
  • apps/posts/test/unit/views/comments/use-filter-state.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/posts/src/views/comments/components/comments-filters.tsx
  • apps/posts/src/views/comments/use-comment-filter-fields.ts
  • apps/posts/test/unit/views/comments/use-filter-state.test.tsx

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a60249ac07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +5 to +11
const LEGACY_OPERATOR_MAP: Record<string, string> = {
is_not: 'is-not',
not_contains: 'does-not-contain',
before: 'is-less',
after: 'is-greater',
on_or_before: 'is-or-less',
on_or_after: 'is-or-greater'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve legacy exact-date operator during filter migration

Legacy comment URLs can contain created_at=is:<date> from the previous comments filter UI, but parseLegacyCommentFilters does not remap is to any supported date operator. That leaves a created_at predicate with operator is, which dateCodec.serialize cannot encode, so the migration pass drops the date clause and rewrites the URL without the user’s date filter. In practice, opening an old moderation link with an exact date silently broadens results to unfiltered comments.

Useful? React with 👍 / 👎.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.19%. Comparing base (11ad8de) to head (344ab8b).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #27043   +/-   ##
=======================================
  Coverage   73.19%   73.19%           
=======================================
  Files        1561     1561           
  Lines      127072   127072           
  Branches    15394    15396    +2     
=======================================
  Hits        93011    93011           
- Misses      33082    33102   +20     
+ Partials      979      959   -20     
Flag Coverage Δ
admin-tests 49.86% <ø> (ø)
e2e-tests 73.19% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives

Comments needed to match the members filter pipeline while keeping legacy single-comment deep links and separating thread and id state from canonical filter URLs.
ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives
Comments author and post filters should stay representable when selected records are deleted or inaccessible, and the follow-up filter-state helpers were simplified without changing the intended comments flow.
ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives
The combined post/page value source should prefer real hydrated labels over synthetic ID fallbacks so comment filters stay readable when one side can still resolve the selected resource.
ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives
The PR introduced two TypeScript regressions in apps/posts: an optional fallback callback was invoked unsafely and the comments page test used a spread signature that failed strict compilation in CI.
ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives
The comments filter follow-up no longer needs a separate Vitest config, so the app test setup now lives in the main Vite config and the new tests were rewritten to rely on the existing @src pathing.
ref #27043
Preserves thread-scoped browsing and exits single-comment mode when row filters are applied
ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives

Aligned comments filtering with members by preserving draft filter state, using site timezone date defaults, keeping thread as URL state, and isolating temporary legacy URL fallback code.
ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives

The comment filters branch should keep the existing Posts Vitest config instead of moving test setup into the Vite config.
ref https://linear.app/ghost/issue/BER-3505/migrate-comment-moderation-filters-to-new-filter-primitives

Aligned comments and members on the shared date codec and removed divergent comment component tests before the filter state refactor.
@jonatansberg jonatansberg force-pushed the ber-3505-comment-filters-migration branch from a60249a to 344ab8b Compare May 5, 2026 12:29
@kevinansfield
Copy link
Copy Markdown
Member

All working fine locally. Only thing I found was the comment text filtering not having any debounce so we fire an API request on every keypress but I think that was an existing issue?

@kevinansfield kevinansfield merged commit 87c233d into main May 6, 2026
46 checks passed
@kevinansfield kevinansfield deleted the ber-3505-comment-filters-migration branch May 6, 2026 17:19
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.

3 participants