Added UI for multiple active paid subscriptions filter#28548
Added UI for multiple active paid subscriptions filter#28548kevinansfield wants to merge 3 commits into
Conversation
WalkthroughThis PR refactors the "multiple active subscriptions" feature from a field-based predicate pattern to a count-based data flow. The filter codec is updated to parse numeric comparators ( Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx build @tryghost/sodo-search |
✅ Succeeded | <1s | View ↗ |
nx build @tryghost/activitypub |
✅ Succeeded | 2s | View ↗ |
nx build @tryghost/signup-form |
✅ Succeeded | <1s | View ↗ |
nx build @tryghost/announcement-bar |
✅ Succeeded | <1s | View ↗ |
nx build @tryghost/comments-ui |
✅ Succeeded | <1s | View ↗ |
nx build @tryghost/admin-toolbar |
✅ Succeeded | <1s | View ↗ |
nx build @tryghost/portal |
✅ Succeeded | <1s | View ↗ |
nx run-many -t test:unit -p @tryghost/posts,@tr... |
✅ Succeeded | 2m 53s | View ↗ |
Additional runs (5) |
✅ Succeeded | ... | View ↗ |
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗
☁️ Nx Cloud last updated this comment at 2026-06-12 18:10:27 UTC
a7d169f to
ba5bf42
Compare
599bd2f to
34e71cc
Compare
ba5bf42 to
01f8351
Compare
34e71cc to
f0ee2e5
Compare
01f8351 to
95e846e
Compare
f0ee2e5 to
74d97aa
Compare
007e5e0 to
c8c2452
Compare
ref https://linear.app/ghost/issue/BER-3720/cleanupimprove-multi-sub-filter-implementation The multiple active subscriptions filter was only reachable through the warning banner's "View members" link and was hidden from the filter bar, because the API only accepted the one hardcoded filter string. Now that the API supports it as a composable filter field, it can behave like any other filter: it appears in the Subscription group after "Member status" as a boolean toggle, shows as a removable filter chip, and can be combined with other filters or inverted to find unaffected members. Like the "Offer" filter, it's only offered when relevant — it uses the same affected-member count as the banner and stays hidden when that count is zero.
ref https://linear.app/ghost/issue/BER-3720/cleanupimprove-multi-sub-filter-implementation The count hook keeps the banner's refetch-on-every-mount settings, but it was also mounted inside MembersFilters, which unmounts and remounts whenever the page swaps between the icon-only and filter-row instances — firing a redundant count request on every add-first/remove-last filter transition. Fetching once in MembersPage and passing the count down means a single request per page visit, and the banner and the filter field availability can never disagree about the count. The banner hook's canManageMembers query gate became redundant since the members route guard already enforces it.
ref https://linear.app/ghost/issue/BER-3720/cleanupimprove-multi-sub-filter-implementation Matches the established Yes/No pattern used by the comments moderation "Reported" filter instead of introducing a one-off boolean toggle. The field is now a select with Yes/No options, so its predicates carry string values like every other select filter, the chip reads "Multiple active subscriptions is Yes" rather than an unlabelled switch, and the bespoke 'boolean' ui type added to the shared filter framework is no longer needed.
c8c2452 to
1b5e7b0
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b5e7b0f3a
ℹ️ 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".
| if (multipleActiveSubscriptionsCount > 0) { | ||
| subscriptionFields.push(createFieldConfig(MULTIPLE_ACTIVE_STRIPE_CUSTOMERS_FIELD)); |
There was a problem hiding this comment.
Keep active multi-subscription filters visible
When an existing URL or saved view contains count.active_stripe_customers:<2 (the new “No” value) and the duplicate-subscription count is 0, this condition removes the field from the filter config even though the predicate is still parsed and serialized by useMembersFilterState. Shade's Filters component renders nothing for active filters whose field config is missing, so the list remains filtered with no visible chip to edit/remove (the same can happen for :>1 after the affected count drops to 0). Consider including this field whenever the current filters contain count.active_stripe_customers, not only when the global count is positive.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts (2)
22-23: 💤 Low valueConsider relaxing aggressive refetch settings.
The combination of
refetchOnMount: 'always'andstaleTime: 0triggers a fresh query on every mount, even if data was just fetched. While this ensures freshness, it may cause unnecessary requests if the component remounts frequently (e.g., during route transitions within the members page).Consider using
refetchOnMount: true(refetches only if stale) or a smallstaleTime(e.g., 30 seconds) to balance freshness and efficiency.🤖 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 `@apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts` around lines 22 - 23, The current hook useMultipleActiveSubscriptionsCount uses aggressive React Query settings (refetchOnMount: 'always' and staleTime: 0) causing a fresh fetch on every mount; change these options to something less aggressive—e.g., set refetchOnMount to true so it only refetches when data is stale and/or set staleTime to a small interval like 30000 (30s) to avoid redundant requests during frequent remounts; update the query options object where refetchOnMount and staleTime are defined to use these new values.
18-18: ⚡ Quick winRemove unnecessary
orderparameter for count-only query.The
order: 'id'parameter adds sorting overhead but serves no purpose in a count-only query that returnsmeta.pagination.total. Removing it will slightly reduce database work.♻️ Proposed fix
const {data} = useBrowseMembers({ searchParams: { filter: MULTIPLE_ACTIVE_STRIPE_CUSTOMERS_FILTER, limit: '1', - fields: 'id', - order: 'id' + fields: 'id' },🤖 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 `@apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts` at line 18, The count-only query in use-multiple-active-subscriptions-count (the hook in use-multiple-active-subscriptions-count.ts) includes an unnecessary ordering param `order: 'id'`; remove that `order` option from the request/options passed to the count query (the call that reads meta.pagination.total) so the query performs a pure count without sorting overhead, leaving all other filters and pagination settings intact.
🤖 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
`@apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts`:
- Line 20: The hook useMultipleActiveSubscriptionsCount currently suppresses
errors by setting defaultErrorHandler: false and effectively returns count: 0 on
failures; change it to surface an unresolved/error state instead of treating
failures as zero by removing/switching defaultErrorHandler handling so the hook
returns an explicit hasResolvedCount (false while loading/errored) and
optionally an error object; then update
callers—useMultipleActiveSubscriptionsBanner and use-member-filter-fields (the
code that reads multipleActiveSubscriptionsCount and hasResolvedCount) to gate
rendering/adding the MULTIPLE_ACTIVE_STRIPE_CUSTOMERS_FILTER and the “We found …
members…” banner on hasResolvedCount being true (or show a
placeholder/alternative UI when unresolved) rather than relying solely on count
> 0.
---
Nitpick comments:
In
`@apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts`:
- Around line 22-23: The current hook useMultipleActiveSubscriptionsCount uses
aggressive React Query settings (refetchOnMount: 'always' and staleTime: 0)
causing a fresh fetch on every mount; change these options to something less
aggressive—e.g., set refetchOnMount to true so it only refetches when data is
stale and/or set staleTime to a small interval like 30000 (30s) to avoid
redundant requests during frequent remounts; update the query options object
where refetchOnMount and staleTime are defined to use these new values.
- Line 18: The count-only query in use-multiple-active-subscriptions-count (the
hook in use-multiple-active-subscriptions-count.ts) includes an unnecessary
ordering param `order: 'id'`; remove that `order` option from the
request/options passed to the count query (the call that reads
meta.pagination.total) so the query performs a pure count without sorting
overhead, leaving all other filters and pagination settings intact.
🪄 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: 258426b6-0b3a-4aad-867c-5a613c0915ac
📒 Files selected for processing (12)
apps/posts/src/views/members/components/members-filters.tsxapps/posts/src/views/members/components/multiple-active-subscriptions-banner.tsxapps/posts/src/views/members/hooks/use-members-filter-state.test.tsxapps/posts/src/views/members/hooks/use-multiple-active-subscriptions-banner.tsapps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.tsapps/posts/src/views/members/member-fields.test.tsapps/posts/src/views/members/member-fields.tsapps/posts/src/views/members/members.tsxapps/posts/src/views/members/multiple-active-subscriptions.test.tsapps/posts/src/views/members/multiple-active-subscriptions.tsapps/posts/src/views/members/use-member-filter-fields.test.tsapps/posts/src/views/members/use-member-filter-fields.ts
💤 Files with no reviewable changes (1)
- apps/posts/src/views/members/multiple-active-subscriptions.test.ts
| fields: 'id', | ||
| order: 'id' | ||
| }, | ||
| defaultErrorHandler: false, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if callers of useMultipleActiveSubscriptionsCount distinguish error vs empty states
rg -nC3 'useMultipleActiveSubscriptionsCount' --type=ts --type=tsxRepository: TryGhost/Ghost
Length of output: 86
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the hook implementation around the referenced line
HOOK_FILE="apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts"
echo "---- HOOK FILE (near defaultErrorHandler) ----"
if [ -f "$HOOK_FILE" ]; then
nl -ba "$HOOK_FILE" | sed -n '1,120p'
else
echo "Missing: $HOOK_FILE" >&2
fi
# Find callers and inspect nearby usage to see how empty vs error are handled
echo "---- CALLERS OF useMultipleActiveSubscriptionsCount ----"
rg -nC3 "useMultipleActiveSubscriptionsCount" -g'*.ts' -g'*.tsx' -g'*.js' -g'*.jsx' apps/posts/src | head -n 200Repository: TryGhost/Ghost
Length of output: 166
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Locate the hook file (avoid assuming exact path exists)
echo "---- FIND HOOK FILE ----"
fd "use-multiple-active-subscriptions-count\.ts" . || true
fd "use-multiple-active-subscriptions-count" apps/posts/src || true
# 2) If found, print the part around defaultErrorHandler (or first 160 lines as fallback)
HOOK_FILE="$(fd -a "use-multiple-active-subscriptions-count\.ts" apps/posts/src . | head -n 1 || true)"
if [ -n "${HOOK_FILE:-}" ] && [ -f "$HOOK_FILE" ]; then
echo "---- HOOK FILE: $HOOK_FILE ----"
# Print up to first 200 lines to include the config area
sed -n '1,220p' "$HOOK_FILE" | cat -n
else
echo "---- HOOK FILE NOT FOUND IN EXPECTED LOCATIONS ----" >&2
fi
# 3) Find call sites
echo "---- CALL SITES ----"
rg -nC3 "useMultipleActiveSubscriptionsCount" apps/posts/src -g'*.ts' -g'*.tsx' -g'*.js' -g'*.jsx' | head -n 200Repository: TryGhost/Ghost
Length of output: 3780
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/posts/src/views/members/members.tsx"
if [ -f "$FILE" ]; then
echo "---- FILE LINES 1-220 ----"
sed -n '1,220p' "$FILE" | cat -n
echo "---- MATCH hasResolvedMultipleActiveSubscriptionsCount ----"
rg -n "hasResolvedMultipleActiveSubscriptionsCount" "$FILE"
echo "---- MATCH multipleActiveSubscriptionsCount ----"
rg -n "multipleActiveSubscriptionsCount" "$FILE"
else
echo "Missing $FILE" >&2
fiRepository: TryGhost/Ghost
Length of output: 13271
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate banner component
echo "---- FIND MultipleActiveSubscriptionsBanner ----"
fd "multiple-active-subscriptions-banner" . || true
fd "MultipleActiveSubscriptionsBanner" . || true
# Print the banner component file if found
BANNER_FILE="$(fd -a "multiple-active-subscriptions-banner\.(t|j)sx?$" . | head -n 1 || true)"
if [ -z "${BANNER_FILE:-}" ]; then
# fallback: search for export function/class
BANNER_FILE="$(rg -l "MultipleActiveSubscriptionsBanner" apps/posts/src | head -n 1 || true)"
fi
echo "---- BANNER_FILE: ${BANNER_FILE:-<none>} ----"
if [ -n "${BANNER_FILE:-}" ]; then
sed -n '1,240p' "$BANNER_FILE" | cat -n
fi
# Find where the filter uses multipleActiveSubscriptionsCount
echo "---- FIND multipleActiveSubscriptionsCount usage in MembersFilters ----"
FILTER_FILE="$(rg -l "multipleActiveSubscriptionsCount" apps/posts/src/views/members/components | head -n 1 || true)"
echo "---- FILTER_FILE: ${FILTER_FILE:-<none>} ----"
if [ -n "${FILTER_FILE:-}" ]; then
sed -n '1,260p' "$FILTER_FILE" | cat -n
fiRepository: TryGhost/Ghost
Length of output: 10606
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-banner.ts"
if [ -f "$FILE" ]; then
sed -n '1,200p' "$FILE" | cat -n
else
echo "Missing $FILE" >&2
fi
# Also check where multipleActiveSubscriptionsCount/hasResolvedCount might affect filter options
echo "---- FIND useMemberFilterFields usage of multipleActiveSubscriptionsCount ----"
rg -n "multipleActiveSubscriptionsCount" apps/posts/src/views/members -g'*.ts' -g'*.tsx' | head -n 50Repository: TryGhost/Ghost
Length of output: 7536
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/posts/src/views/members/use-member-filter-fields.ts"
if [ ! -f "$FILE" ]; then
echo "Missing $FILE" >&2
exit 1
fi
# Print relevant sections where multipleActiveSubscriptionsCount is used
# (Based on earlier rg line hits around 257, 389)
sed -n '220,320p' "$FILE" | cat -n
echo "----"
sed -n '360,520p' "$FILE" | cat -nRepository: TryGhost/Ghost
Length of output: 9083
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# locate the multiple-active-subscriptions module
fd "multiple-active-subscriptions" apps/posts/src/views/members -a || true
fd "multiple-active-subscriptions\.(t|j)sx?$" apps/posts/src/views/members -a || true
FILE="$(fd -a "multiple-active-subscriptions\.(ts|tsx|js|jsx)$" apps/posts/src/views/members . | head -n 1 || true)"
if [ -z "${FILE:-}" ]; then
FILE="$(rg -l "MULTIPLE_ACTIVE_STRIPE_CUSTOMERS_FIELD" apps/posts/src/views/members | head -n 1 || true)"
fi
echo "---- MODULE FILE: ${FILE:-<none>} ----"
if [ -n "${FILE:-}" ]; then
sed -n '1,260p' "$FILE" | cat -n
fiRepository: TryGhost/Ghost
Length of output: 3982
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/posts/src/views/members/members.tsx"
sed -n '210,290p' "$FILE" | cat -nRepository: TryGhost/Ghost
Length of output: 4569
Guard against treating “count” query failures as zero; otherwise banner/filter UX can be misleading.
defaultErrorHandler: false in apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts suppresses errors and the hook returns count: 0 with hasResolvedCount: false when the count request fails.
apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-banner.tsusescountto render “We found … members…” and shows the banner when viewingMULTIPLE_ACTIVE_STRIPE_CUSTOMERS_FILTEReven ifhasResolvedCountis false, so a count failure can display0despite the filtered member list having results.apps/posts/src/views/members/use-member-filter-fields.tsonly adds the multi-active-subscriptions filter field whenmultipleActiveSubscriptionsCount > 0, so a count failure hides the filter option.
Consider surfacing an error/unresolved state from the hook and gating banner/message (and the filter field) on hasResolvedCount (or using a placeholder/alternative UI when unresolved).
🤖 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
`@apps/posts/src/views/members/hooks/use-multiple-active-subscriptions-count.ts`
at line 20, The hook useMultipleActiveSubscriptionsCount currently suppresses
errors by setting defaultErrorHandler: false and effectively returns count: 0 on
failures; change it to surface an unresolved/error state instead of treating
failures as zero by removing/switching defaultErrorHandler handling so the hook
returns an explicit hasResolvedCount (false while loading/errored) and
optionally an error object; then update
callers—useMultipleActiveSubscriptionsBanner and use-member-filter-fields (the
code that reads multipleActiveSubscriptionsCount and hasResolvedCount) to gate
rendering/adding the MULTIPLE_ACTIVE_STRIPE_CUSTOMERS_FILTER and the “We found …
members…” banner on hasResolvedCount being true (or show a
placeholder/alternative UI when unresolved) rather than relying solely on count
> 0.

ref https://linear.app/ghost/issue/BER-3720/cleanupimprove-multi-sub-filter-implementation
The multiple active subscriptions filter was only reachable through the warning banner's "View members" link and was deliberately hidden from the filter bar, because the API only accepted the one hardcoded
count.active_stripe_customers:>1filter string — exposing it in the UI would have produced unrepresentable or broken states.With #28547 making the filter a composable NQL field, that restriction is gone, so this makes it behave like any other members filter:
count.active_stripe_customers:>1; No inverts it tocount.active_stripe_customers:<2to find unaffected members.useMultipleActiveSubscriptionsCount) and fetched once at page level, and the filter stays hidden while that count is zero.