Added gift links admin UI for analytics, posts list, and settings#28897
Added gift links admin UI for analytics, posts list, and settings#28897jonatansberg wants to merge 2 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (4)
WalkthroughThis PR adds gift-link management across admin and posts. It introduces gift-link API client mutations, a settings action to reset all gift links, an Ember-to-React bridge for opening a React gift-link modal from posts and pages lists, and new posts-app hooks, utilities, modal UI, and sharing entry points. It also adds related tests, package exports, and route updates. Possibly related PRs
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)
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 |
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx run @tryghost/admin-x-settings:test:acceptance |
✅ Succeeded | 10m 19s | View ↗ |
nx run-many --target=build --projects=tag:publi... |
✅ Succeeded | 1s | View ↗ |
nx run-many -t test:unit -p @tryghost/admin-x-f... |
✅ Succeeded | 7m 40s | View ↗ |
nx run @tryghost/admin:build |
✅ Succeeded | 4m 42s | View ↗ |
nx run ghost-admin:test |
✅ Succeeded | 3m 15s | View ↗ |
nx run-many -t lint -p @tryghost/admin-x-framew... |
✅ Succeeded | 1m 11s | View ↗ |
nx run @tryghost/activitypub:test:acceptance |
✅ Succeeded | 57s | View ↗ |
nx run ghost:build:assets |
✅ Succeeded | 2s | View ↗ |
nx run ghost:build:tsc |
✅ Succeeded | 6s | View ↗ |
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗
☁️ Nx Cloud last updated this comment at 2026-06-25 16:18:00 UTC
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/posts/src/utils/gift-link.ts (1)
7-12: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueEdge case: hard-coded
?breaks URLs that already carry a query string.
buildGiftLinkUrlalways prefixes with?, so apostUrlalready containing a query string would yield a malformed URL with two?. Canonical post URLs are normally clean, so this is just defensive hardening.♻️ Use the correct separator
- return `${postUrl}?gift=${encodeURIComponent(token)}`; + const separator = postUrl.includes('?') ? '&' : '?'; + return `${postUrl}${separator}gift=${encodeURIComponent(token)}`;🤖 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/utils/gift-link.ts` around lines 7 - 12, The buildGiftLinkUrl helper always appends the gift token with a hard-coded query separator, which breaks when postUrl already contains existing query parameters. Update buildGiftLinkUrl to choose the correct separator based on whether postUrl already includes a query string, and keep the token encoding behavior unchanged.
🤖 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/PostAnalytics/modals/gift-link-modal.tsx`:
- Around line 135-143: Disable the ShareModal.CopyButton in gift-link-modal
while the link is being generated so it cannot copy an empty string. Update the
copy button in the gift-link CopyURLBox to use the existing ensuring state from
the modal logic, and keep the disabled state aligned with the same
giftLinkUrl/ensuring flow used in this component.
---
Nitpick comments:
In `@apps/posts/src/utils/gift-link.ts`:
- Around line 7-12: The buildGiftLinkUrl helper always appends the gift token
with a hard-coded query separator, which breaks when postUrl already contains
existing query parameters. Update buildGiftLinkUrl to choose the correct
separator based on whether postUrl already includes a query string, and keep the
token encoding behavior unchanged.
🪄 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: 9397fe1c-217a-4d38-88dc-8ce6dd3e7870
📒 Files selected for processing (24)
apps/admin-x-framework/src/api/gift-links.tsapps/admin-x-settings/src/components/settings/advanced/danger-zone.tsxapps/admin-x-settings/test/acceptance/advanced/dangerzone.test.tsapps/admin/src/ember-bridge/ember-bridge.tsxapps/admin/src/ember-bridge/index.tsapps/admin/src/gift-link-modal-host.tsxapps/admin/src/routes.tsxapps/posts/package.jsonapps/posts/src/hooks/use-can-manage-gift-link.tsapps/posts/src/hooks/use-gift-link-usage.tsapps/posts/src/hooks/use-post-details.tsapps/posts/src/providers/post-analytics-context.tsxapps/posts/src/utils/constants.tsapps/posts/src/utils/gift-link.tsapps/posts/src/views/PostAnalytics/components/post-analytics-header.tsxapps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsxapps/posts/test/unit/utils/gift-link.test.tsapps/shade/src/components/posts-stats/post-share-modal.tsxghost/admin/app/components/posts-list/context-menu.hbsghost/admin/app/components/posts-list/context-menu.jsghost/admin/app/services/feature.jsghost/admin/app/services/state-bridge.jsghost/admin/app/utils/gift-link.jsghost/admin/tests/unit/utils/gift-link-test.js
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e4e3aba258
ℹ️ 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".
88b5608 to
cd8049c
Compare
ref https://linear.app/ghost/issue/BER-3729 - one shared React gift-link modal is reused across the post-analytics screen and the Ember posts/pages list, rather than maintaining a separate Ember modal: the list's right-click menu fires an `openGiftLinkModal` event over the state bridge and a host mounted alongside the Ember fallback opens the React modal in place - the modal fetches link details (admin API) and usage (the same Tinybird analytics path as everything else) separately, degrading to no visitor count when analytics is off or the usage pipe isn't deployed yet; the share URL is the canonical post URL + `?gift=<token>` - adds the danger-zone "reset all gift links" action and a "share as a gift" entry in the post share modal - all gated behind the existing private `giftLinks` flag
cd8049c to
0f9b693
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f9b6932f5
ℹ️ 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".
| }), [statsConfig?.id, postUuid]); | ||
|
|
||
| const {data, loading, error} = useTinybirdQuery({ | ||
| endpoint: 'api_gift_link_visits', |
There was a problem hiding this comment.
Add the Tinybird pipe before querying it
With analytics enabled, this hook asks Tinybird for api_gift_link_visits, but repo-wide rg "api_gift_link_visits" ghost/core/core/server/data/tinybird finds no endpoint datafile, while getStatEndpointUrl resolves endpoint names to /v0/pipes/<name>.json. As a result every gift-link card/modal calls a non-existent pipe and the visitor count stays hidden/— even when visits exist; add the Tinybird endpoint (and any versioned variants required by statsConfig.version) or gate this query until it exists.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/PostAnalytics/Overview/overview.tsx`:
- Around line 218-225: The “Share” button in PostAnalytics/Overview is only
revealed on hover, so it stays hidden for keyboard and touch users. Update the
Button in the relevant render path to also reveal on focus-visible (mirroring
the existing Growth “View more” behavior if applicable), while keeping the
current hover animation intact. Use the Button element and its existing
className/variant setup as the reference point, and apply the same accessibility
fix wherever the same hover-only pattern appears.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 718f5ac2-0d5e-458f-b497-8aa983b49480
📒 Files selected for processing (25)
apps/admin-x-framework/src/api/gift-links.tsapps/admin-x-settings/src/components/settings/advanced/danger-zone.tsxapps/admin-x-settings/test/acceptance/advanced/dangerzone.test.tsapps/admin/src/ember-bridge/ember-bridge.tsxapps/admin/src/ember-bridge/index.tsapps/admin/src/gift-link-modal-host.tsxapps/admin/src/routes.tsxapps/posts/package.jsonapps/posts/src/hooks/use-can-manage-gift-link.tsapps/posts/src/hooks/use-gift-link-usage.tsapps/posts/src/hooks/use-post-details.tsapps/posts/src/providers/post-analytics-context.tsxapps/posts/src/utils/constants.tsapps/posts/src/utils/gift-link.tsapps/posts/src/views/PostAnalytics/Overview/overview.tsxapps/posts/src/views/PostAnalytics/components/post-analytics-header.tsxapps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsxapps/posts/test/unit/utils/gift-link.test.tsapps/shade/src/components/posts-stats/post-share-modal.tsxghost/admin/app/components/posts-list/context-menu.hbsghost/admin/app/components/posts-list/context-menu.jsghost/admin/app/services/feature.jsghost/admin/app/services/state-bridge.jsghost/admin/app/utils/gift-link.jsghost/admin/tests/unit/utils/gift-link-test.js
✅ Files skipped from review due to trivial changes (3)
- apps/admin/src/ember-bridge/index.ts
- apps/posts/src/providers/post-analytics-context.tsx
- apps/posts/test/unit/utils/gift-link.test.ts
🚧 Files skipped from review as they are similar to previous changes (21)
- apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts
- apps/posts/src/utils/gift-link.ts
- apps/posts/package.json
- apps/posts/src/hooks/use-can-manage-gift-link.ts
- ghost/admin/app/components/posts-list/context-menu.hbs
- ghost/admin/app/services/feature.js
- apps/admin/src/routes.tsx
- apps/admin/src/ember-bridge/ember-bridge.tsx
- apps/admin-x-framework/src/api/gift-links.ts
- apps/posts/src/hooks/use-post-details.ts
- apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
- apps/posts/src/utils/constants.ts
- ghost/admin/app/utils/gift-link.js
- ghost/admin/app/components/posts-list/context-menu.js
- apps/shade/src/components/posts-stats/post-share-modal.tsx
- ghost/admin/app/services/state-bridge.js
- ghost/admin/tests/unit/utils/gift-link-test.js
- apps/posts/src/hooks/use-gift-link-usage.ts
- apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx
- apps/admin/src/gift-link-modal-host.tsx
- apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
| <Button | ||
| className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100' | ||
| size='sm' | ||
| variant='outline' | ||
| onClick={() => setIsGiftLinkOpen(true)} | ||
| > | ||
| Share | ||
| </Button> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
“Share” button is only visible on hover, with no keyboard/touch fallback.
opacity-0 + group-hover/datalist:opacity-100 leaves the button focusable and clickable but invisible for keyboard users (focus lands on an invisible control) and touch devices (no hover). Adding a focus-visible reveal keeps it discoverable while preserving the hover animation. This mirrors the existing Growth “View more” button, so consider it there too.
♿ Reveal on focus as well as hover
<Button
- className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100'
+ className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100 focus-visible:translate-x-0 focus-visible:opacity-100'
size='sm'
variant='outline'
onClick={() => setIsGiftLinkOpen(true)}
>
Share
</Button>📝 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.
| <Button | |
| className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100' | |
| size='sm' | |
| variant='outline' | |
| onClick={() => setIsGiftLinkOpen(true)} | |
| > | |
| Share | |
| </Button> | |
| <Button | |
| className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100 focus-visible:translate-x-0 focus-visible:opacity-100' | |
| size='sm' | |
| variant='outline' | |
| onClick={() => setIsGiftLinkOpen(true)} | |
| > | |
| Share | |
| </Button> |
🤖 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/PostAnalytics/Overview/overview.tsx` around lines 218 -
225, The “Share” button in PostAnalytics/Overview is only revealed on hover, so
it stays hidden for keyboard and touch users. Update the Button in the relevant
render path to also reveal on focus-visible (mirroring the existing Growth “View
more” behavior if applicable), while keeping the current hover animation intact.
Use the Button element and its existing className/variant setup as the reference
point, and apply the same accessibility fix wherever the same hover-only pattern
appears.

ref https://linear.app/ghost/issue/BER-3729
Summary
Adds the publisher-facing gift-links admin UI on top of the service + admin API already on
main. A single React gift-link modal is reused across surfaces instead of maintaining a separate Ember modal:openGiftLinkModalevent over the state bridge; a host mounted alongside the Ember fallback (at the/postsand/pagesroutes) opens the React modal in place.All gated behind the existing private
giftLinksflag.Notes
gift_links) and usage (visits/views via the same Tinybird analytics path as every other analytics surface). Usage degrades gracefully — when analytics is off, or the per-link usage pipe (BER-3746/BER-3728) isn't deployed yet, the visitor count is simply hidden and everything else still works.?gift=<token>(noutm).POST_ANALYTICS_INCLUDE); pages fetch on their own route. API wrapper names follow the backend controllers (ensure/create/removeAll).Testing
apps/postsunit suite (482) green; new URL-builder unit test; Ember eligibility-util unit test.admin-x-settingsdanger-zone acceptance test for reset-all (passing locally).posts,admin,shade,admin-x-framework,admin-x-settings, andghost/admin.