Skip to content

Add archived tiers to tier filter#28189

Open
jonatansberg wants to merge 1 commit into
mainfrom
ber-3588-add-archived-tiers-to-tiers-filter
Open

Add archived tiers to tier filter#28189
jonatansberg wants to merge 1 commit into
mainfrom
ber-3588-add-archived-tiers-to-tiers-filter

Conversation

@jonatansberg
Copy link
Copy Markdown
Member

@jonatansberg jonatansberg commented May 27, 2026

Summary

  • Added grouped Shade filter option support for multiselect lists, including group-heading search behavior.
  • Wrapped member tier fetching, paging, option mapping, and tier-count visibility inside useTierValueSource, matching the other member filter value sources.
  • Updated the members tier filter to include paid active and archived tiers grouped as Active tiers and Archived tiers, without using deprecated limit=all.
  • Replaced the members component-level unit test with business-logic and hook unit coverage, plus e2e coverage for the user interaction.

Why

Members can retain archived paid tiers, but the members filter only exposed active paid tiers. That made archived-tier member segments impossible to select from the tier filter.

Impact

The Membership tier filter now shows active and archived paid tiers in separate sections. Member table values continue to display the actual tier name, including archived tier names.

Validation

  • pnpm --filter @tryghost/shade exec vitest run test/unit/components/patterns/filters.test.tsx
  • pnpm --filter @tryghost/posts exec vitest run test/unit/hooks/use-tier-value-source.test.tsx test/unit/views/members/member-query-params.test.ts src/views/members/use-member-filter-fields.test.ts
  • pnpm --filter @tryghost/posts lint:code
  • pnpm --filter @tryghost/posts lint:test
  • pnpm --filter @tryghost/e2e lint
  • pnpm --filter @tryghost/e2e test:types
  • git diff --check origin/main..HEAD

Notes:

  • Posts lint passes with the existing unrelated warning in apps/posts/src/hooks/use-post-success-modal.ts.
  • E2E lint passes with existing unrelated warnings.
  • The focused e2e run was attempted with pnpm --filter @tryghost/e2e test tests/admin/members/tier-filter-search.test.ts, but this temp worktree's dev-mode Ghost container failed before tests ran because ghost/core has no local node_modules (nodemon: not found).

ref https://linear.app/ghost/issue/BER-3588/add-archived-tiers-to-tiers-filter

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Review Change Stack

Walkthrough

The PR refactors useTierValueSource into a self-contained hook that calls useBrowseTiers for paid tiers (limit/filter), auto-paginates, builds grouped filter options (active first, archived labeled “(archived)”), and exposes a local ValueSource augmented with hasMultipleTiers. MembersFilters no longer fetches tiers directly and reads hasMultipleTiers from the hook. Unit tests were added to validate option ordering, searching, hasMultipleTiers logic (including pagination), initial-load behavior, and fetchNextPage control.

🚥 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 'Add archived tiers to tier filter' directly matches the main objective: exposing archived tiers in the members tier filter alongside active tiers.
Description check ✅ Passed The description comprehensively explains the changes (grouped Shade filter support, useTierValueSource implementation, archived tier inclusion) and provides clear context on why the changes were made and their 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ber-3588-add-archived-tiers-to-tiers-filter

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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.

@jonatansberg jonatansberg force-pushed the ber-3588-add-archived-tiers-to-tiers-filter branch 3 times, most recently from 0cecfde to 8e2a7b7 Compare May 27, 2026 13:21
@jonatansberg jonatansberg changed the title [codex] Add archived tiers to tier filter Add archived tiers to tier filter May 27, 2026
@jonatansberg jonatansberg force-pushed the ber-3588-add-archived-tiers-to-tiers-filter branch 3 times, most recently from 3e7a1b8 to 343c22d Compare May 28, 2026 09:02
@jonatansberg jonatansberg marked this pull request as ready for review May 28, 2026 13:36
@jonatansberg jonatansberg requested a review from 9larsons as a code owner May 28, 2026 13:36
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: 0

🧹 Nitpick comments (1)
apps/posts/test/unit/views/members/member-query-params.test.ts (1)

29-49: ⚡ Quick win

Consider adding a test case for multiple tiers to verify join behavior.

The implementation joins multiple tier names with ', ', but this test only covers a single tier. Adding a test with multiple tiers would ensure the join logic works correctly and that all tier names are displayed (not just the first).

✨ Example test case for multiple tiers
+    it('joins multiple tier names with comma separator', () => {
+        const member = {
+            tiers: [
+                {
+                    id: 'tier-1',
+                    name: 'Archived Gold',
+                    slug: 'archived-gold',
+                    active: false,
+                    type: 'paid'
+                },
+                {
+                    id: 'tier-2',
+                    name: 'Premium Silver',
+                    slug: 'premium-silver',
+                    active: true,
+                    type: 'paid'
+                }
+            ]
+        } as Member;
+
+        expect(getActiveColumnValue({
+            key: 'tiers',
+            label: 'Tiers',
+            include: 'tiers'
+        }, member, 'UTC')).toEqual({
+            text: 'Archived Gold, Premium Silver'
+        });
+    });
🤖 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/test/unit/views/members/member-query-params.test.ts` around lines
29 - 49, Add a unit test in member-query-params.test.ts that covers multiple
tiers to verify the join behavior: create a Member object with tiers array
containing multiple tier objects with distinct name fields, call
getActiveColumnValue({key: 'tiers', label: 'Tiers', include: 'tiers'}, member,
'UTC') and assert the returned value.text equals the tier names joined with ", "
(e.g., "Gold, Silver"); this ensures getActiveColumnValue correctly concatenates
multiple tier names rather than only returning the first.
🤖 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 `@apps/posts/test/unit/views/members/member-query-params.test.ts`:
- Around line 29-49: Add a unit test in member-query-params.test.ts that covers
multiple tiers to verify the join behavior: create a Member object with tiers
array containing multiple tier objects with distinct name fields, call
getActiveColumnValue({key: 'tiers', label: 'Tiers', include: 'tiers'}, member,
'UTC') and assert the returned value.text equals the tier names joined with ", "
(e.g., "Gold, Silver"); this ensures getActiveColumnValue correctly concatenates
multiple tier names rather than only returning the first.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2b7b3497-cdd0-4f87-beb5-013af4210600

📥 Commits

Reviewing files that changed from the base of the PR and between 8bdc9fe and 343c22d.

📒 Files selected for processing (7)
  • apps/posts/src/hooks/filter-sources/use-tier-value-source.ts
  • apps/posts/src/hooks/filter-sources/utils.ts
  • apps/posts/src/views/members/components/members-filters.tsx
  • apps/posts/test/unit/hooks/use-tier-value-source.test.tsx
  • apps/posts/test/unit/views/members/member-query-params.test.ts
  • apps/shade/src/components/patterns/filters.tsx
  • e2e/tests/admin/members/tier-filter-search.test.ts

@jonatansberg jonatansberg force-pushed the ber-3588-add-archived-tiers-to-tiers-filter branch 2 times, most recently from b8707cc to 01d87da Compare May 29, 2026 13:53
ref https://linear.app/ghost/issue/BER-3588/add-archived-tiers-to-tiers-filter

Members can keep archived paid tiers, so the filter needs to preserve those options while separating them from active tiers.
@jonatansberg jonatansberg force-pushed the ber-3588-add-archived-tiers-to-tiers-filter branch from 01d87da to 89509c1 Compare May 29, 2026 14:09
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)
apps/posts/src/hooks/filter-sources/use-tier-value-source.ts (1)

53-65: ⚖️ Poor tradeoff

createLocalValueSource is rebuilt on every render in useTierValueSource.

useTierValueSource calls createLocalValueSource(...) inside the hook body and returns a new tierValueSource object each render (new useOptions closure identity). tierValueSource is then part of the useMemberFilterFields useMemo dependency list, so this can force recomputation of filter fields even when the underlying tier options didn’t change. Consider memoizing the created local value source (or the returned tierValueSource) based on options/isLoadingTierOptions.

🤖 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/hooks/filter-sources/use-tier-value-source.ts` around lines 53
- 65, useTierValueSource currently calls createLocalValueSource(...) on every
render which yields a new useLocalTierValueSource closure identity and forces
recomputation in downstream hooks; change it to memoize the created local value
source (or the final returned tierValueSource) using React.useMemo so the
identity is stable unless the inputs change — base the memo on options and
isLoadingTierOptions (and hasMultipleTiers if it affects consumers) and keep the
same shape (id, useItems, toOption) so useMemberFilterFields sees a stable
reference unless the underlying tier data truly changes.
🤖 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/hooks/filter-sources/use-tier-value-source.ts`:
- Around line 16-22: toTierFilterOption currently appends the hardcoded
ARCHIVED_TIER_LABEL_SUFFIX to build FilterOption.label; change this to use the
app's i18n translator so the archived suffix is localized (e.g., use the
translation key with interpolation like "{name} (archived)"). Update the
toTierFilterOption implementation to call the translation function (or accept a
t/translate argument) and pass tier.name into the interpolated string instead of
concatenating ARCHIVED_TIER_LABEL_SUFFIX, and remove reliance on the hardcoded
ARCHIVED_TIER_LABEL_SUFFIX constant so labels are produced via i18n.

---

Nitpick comments:
In `@apps/posts/src/hooks/filter-sources/use-tier-value-source.ts`:
- Around line 53-65: useTierValueSource currently calls
createLocalValueSource(...) on every render which yields a new
useLocalTierValueSource closure identity and forces recomputation in downstream
hooks; change it to memoize the created local value source (or the final
returned tierValueSource) using React.useMemo so the identity is stable unless
the inputs change — base the memo on options and isLoadingTierOptions (and
hasMultipleTiers if it affects consumers) and keep the same shape (id, useItems,
toOption) so useMemberFilterFields sees a stable reference unless the underlying
tier data truly changes.
🪄 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: 7189a7e4-0c3b-4c28-bdac-f61b9bfecce9

📥 Commits

Reviewing files that changed from the base of the PR and between 01d87da and 89509c1.

📒 Files selected for processing (4)
  • apps/posts/src/hooks/filter-sources/use-tier-value-source.ts
  • apps/posts/src/views/members/components/members-filters.tsx
  • apps/posts/src/views/members/use-member-filter-fields.test.ts
  • apps/posts/test/unit/hooks/use-tier-value-source.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/posts/src/views/members/components/members-filters.tsx

Comment on lines +16 to +22
function toTierFilterOption(tier: Tier): FilterOption<string> {
return {
value: tier.id,
label: tier.active ? tier.name : `${tier.name}${ARCHIVED_TIER_LABEL_SUFFIX}`,
detail: tier.slug
};
}
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for translation usage in apps/posts
rg -nP "\bt\(\s*['\"]" apps/posts/src | head -50
fd -t f -e ts -e tsx . apps/posts/src | xargs rg -nl "useTranslation|`@doist/react-interpolate`|import.*\bt\b.*i18n" 2>/dev/null | head -20

Repository: TryGhost/Ghost

Length of output: 40


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the suffix constant definition/usages
rg -n "ARCHIVED_TIER_LABEL_SUFFIX" -S apps/posts/src

# 2) Inspect the actual implementation around the reported lines
sed -n '1,120p' apps/posts/src/hooks/filter-sources/use-tier-value-source.ts

# 3) Broader check: find i18n usage patterns in apps/posts/src (not just `t(`)
rg -n "useTranslation|`@doist/react-interpolate`|react-interpolate|i18n|next-i18next|formatMessage|intl|translate" apps/posts/src || true

# 4) Check for any `t('...')` usage and capture a few examples
rg -nP "\bt\(\s*['\"]" apps/posts/src | head -50 || true

# 5) If no `t(` usage, check for direct user-facing English strings near tiers/filters
rg -n "(archived|Archived|\(archived\)|filter.*tier|tier.*(archived|archived))" apps/posts/src || true

Repository: TryGhost/Ghost

Length of output: 8507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Translation helper presence checks in apps/posts
rg -n --hidden -S "useTranslation|react-i18next|i18next|formatMessage|useIntl|next-intl|`@doist/react-interpolate`|react-interpolate|declare.*t\(|\bt\(\s*[`'\"]|\bIntl\." apps/posts/src || true

# Specifically check for any t() usage patterns (including template literals)
rg -nP "\bt\(\s*[`'\"].{0,200}\)" apps/posts/src || true

# Check for translation dictionaries / message IDs (common in this codebase style)
rg -n "messages|i18n\.[a-zA-Z_]+|translations|localized|locale" apps/posts/src || true

Repository: TryGhost/Ghost

Length of output: 4268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== t('...') usage in apps/posts/src =="
rg -n "\bt\(\s*['\"]" apps/posts/src || true

echo "== translation helper usage (various) in apps/posts/src =="
rg -n "useTranslation\(|react-i18next|i18next|formatMessage|useIntl\(|next-intl|`@doist/react-interpolate`|react-interpolate" apps/posts/src || true

echo "== literal archived suffix usage in apps/posts/src =="
rg -n "(\(archived\)|archived\)|' \(archived\)'|\" \(archived\)\")" apps/posts/src || true

echo "== constant usage =="
rg -n "ARCHIVED_TIER_LABEL_SUFFIX" apps/posts/src || true

Repository: TryGhost/Ghost

Length of output: 607


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "posts\.tiers\.local" apps/posts/src || true
rg -n "posts\.tiers" apps/posts/src || true
rg -n "FilterOption<" apps/posts/src || true
rg -n "\.detail\b|\.label\b" apps/posts/src/views/filters apps/posts/src/views/members apps/posts/src/components apps/posts/src/hooks/filter-sources || true

Repository: TryGhost/Ghost

Length of output: 4121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for other i18n patterns used in posts UI (not only t('...'))
rg -n "i18n=\{|\bi18n=\{|\bi18n:\s*\{|<.*\bi18n=" apps/posts/src || true
rg -n "from '.*i18n|from \".*i18n|from '.*translate|from \".*translate" apps/posts/src || true
rg -n "translate\(|formatMessage\(|intl\." apps/posts/src || true

Repository: TryGhost/Ghost

Length of output: 784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '130,190p' apps/posts/src/components/label-picker/label-filter-renderer.tsx
sed -n '220,270p' apps/posts/src/views/members/use-member-filter-fields.ts

Repository: TryGhost/Ghost

Length of output: 3173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where tierValueSource is consumed in the member filter fields hook
rg -n "tierValueSource" apps/posts/src/views/members/use-member-filter-fields.ts

# Inspect the surrounding logic (small targeted ranges)
sed -n '1,120p' apps/posts/src/views/members/use-member-filter-fields.ts
sed -n '120,240p' apps/posts/src/views/members/use-member-filter-fields.ts
sed -n '240,360p' apps/posts/src/views/members/use-member-filter-fields.ts
sed -n '360,520p' apps/posts/src/views/members/use-member-filter-fields.ts

Repository: TryGhost/Ghost

Length of output: 16029


Localize the archived tier label suffix instead of hardcoding English

ARCHIVED_TIER_LABEL_SUFFIX is hardcoded to ' (archived)' and concatenated into FilterOption.label:

function toTierFilterOption(tier: Tier): FilterOption<string> {
    return {
        value: tier.id,
        label: tier.active ? tier.name : `${tier.name}${ARCHIVED_TIER_LABEL_SUFFIX}`,
        detail: tier.slug
    };
}

Because this string is constructed outside the app’s i18n layer, the “(archived)” part won’t be translated for localized users. Build the archived label via the app’s translation mechanism instead, using a single translatable string with interpolation (e.g. {name} (archived)).

🤖 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/hooks/filter-sources/use-tier-value-source.ts` around lines 16
- 22, toTierFilterOption currently appends the hardcoded
ARCHIVED_TIER_LABEL_SUFFIX to build FilterOption.label; change this to use the
app's i18n translator so the archived suffix is localized (e.g., use the
translation key with interpolation like "{name} (archived)"). Update the
toTierFilterOption implementation to call the translation function (or accept a
t/translate argument) and pass tier.name into the interpolated string instead of
concatenating ARCHIVED_TIER_LABEL_SUFFIX, and remove reliance on the hardcoded
ARCHIVED_TIER_LABEL_SUFFIX constant so labels are produced via i18n.

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.

1 participant