Skip to content

Added gift links admin UI for analytics, posts list, and settings#28897

Open
jonatansberg wants to merge 2 commits into
mainfrom
jonatan-ber-3729-gift-links-admin-ui-editor-settings
Open

Added gift links admin UI for analytics, posts list, and settings#28897
jonatansberg wants to merge 2 commits into
mainfrom
jonatan-ber-3729-gift-links-admin-ui-editor-settings

Conversation

@jonatansberg

Copy link
Copy Markdown
Member

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:

  • Post analytics screen — a "Share as a gift" entry in the post share modal opens the modal.
  • Ember posts/pages list — the right-click context menu fires an openGiftLinkModal event over the state bridge; a host mounted alongside the Ember fallback (at the /posts and /pages routes) opens the React modal in place.
  • Settings → Advanced → Danger zone — a "Reset all gift links" action.

All gated behind the existing private giftLinks flag.

Notes

  • The modal makes two separate reads: link details (admin API 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.
  • Share URL is the canonical post URL + ?gift=<token> (no utm).
  • Posts reuse the post-analytics screen's cached query (shared POST_ANALYTICS_INCLUDE); pages fetch on their own route. API wrapper names follow the backend controllers (ensure / create / removeAll).

Testing

  • apps/posts unit suite (482) green; new URL-builder unit test; Ember eligibility-util unit test.
  • admin-x-settings danger-zone acceptance test for reset-all (passing locally).
  • Typecheck + lint clean across posts, admin, shade, admin-x-framework, admin-x-settings, and ghost/admin.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b322676-0332-45d2-8fcc-ee70fc0ab1bd

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9b693 and cb0bb20.

⛔ Files ignored due to path filters (1)
  • ghost/admin/public/assets/icons/gift-link.svg is excluded by !**/*.svg
📒 Files selected for processing (5)
  • apps/posts/src/views/PostAnalytics/Overview/overview.tsx
  • apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
  • ghost/admin/app/components/posts-list/context-menu.hbs
  • ghost/admin/app/styles/components/dropdowns.css
✅ Files skipped from review due to trivial changes (1)
  • ghost/admin/app/styles/components/dropdowns.css
🚧 Files skipped from review as they are similar to previous changes (4)
  • ghost/admin/app/components/posts-list/context-menu.hbs
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
  • apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
  • apps/posts/src/views/PostAnalytics/Overview/overview.tsx

Walkthrough

This 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

  • TryGhost/Ghost#28784 — Adds the admin API client surface for gift-link operations, including resource-scoped paths and remove-all handling.
  • TryGhost/Ghost#28693 — Adds backend gift-link routes and response shapes that match the client mutations used here.

Suggested reviewers

  • kevinansfield
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change set: gift-links admin UI across analytics, lists, and settings.
Description check ✅ Passed The description matches the changes and clearly explains the gift-links modal, bridge, and settings work.
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 jonatan-ber-3729-gift-links-admin-ui-editor-settings

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.

@nx-cloud

nx-cloud Bot commented Jun 25, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit cb0bb20

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/posts/src/utils/gift-link.ts (1)

7-12: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Edge case: hard-coded ? breaks URLs that already carry a query string.

buildGiftLinkUrl always prefixes with ?, so a postUrl already 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

📥 Commits

Reviewing files that changed from the base of the PR and between 515c39b and e4e3aba.

📒 Files selected for processing (24)
  • apps/admin-x-framework/src/api/gift-links.ts
  • apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx
  • apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts
  • apps/admin/src/ember-bridge/ember-bridge.tsx
  • apps/admin/src/ember-bridge/index.ts
  • apps/admin/src/gift-link-modal-host.tsx
  • apps/admin/src/routes.tsx
  • apps/posts/package.json
  • apps/posts/src/hooks/use-can-manage-gift-link.ts
  • apps/posts/src/hooks/use-gift-link-usage.ts
  • apps/posts/src/hooks/use-post-details.ts
  • apps/posts/src/providers/post-analytics-context.tsx
  • apps/posts/src/utils/constants.ts
  • apps/posts/src/utils/gift-link.ts
  • apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
  • apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
  • apps/posts/test/unit/utils/gift-link.test.ts
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
  • ghost/admin/app/components/posts-list/context-menu.hbs
  • ghost/admin/app/components/posts-list/context-menu.js
  • ghost/admin/app/services/feature.js
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/app/utils/gift-link.js
  • ghost/admin/tests/unit/utils/gift-link-test.js

Comment thread apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx

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

Copy link
Copy Markdown

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: 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".

Comment thread apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
@jonatansberg jonatansberg force-pushed the jonatan-ber-3729-gift-links-admin-ui-editor-settings branch 2 times, most recently from 88b5608 to cd8049c Compare June 25, 2026 13:01
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
@jonatansberg jonatansberg force-pushed the jonatan-ber-3729-gift-links-admin-ui-editor-settings branch from cd8049c to 0f9b693 Compare June 25, 2026 14:30

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

Copy link
Copy Markdown

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: 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',

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 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between cd8049c and 0f9b693.

📒 Files selected for processing (25)
  • apps/admin-x-framework/src/api/gift-links.ts
  • apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx
  • apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts
  • apps/admin/src/ember-bridge/ember-bridge.tsx
  • apps/admin/src/ember-bridge/index.ts
  • apps/admin/src/gift-link-modal-host.tsx
  • apps/admin/src/routes.tsx
  • apps/posts/package.json
  • apps/posts/src/hooks/use-can-manage-gift-link.ts
  • apps/posts/src/hooks/use-gift-link-usage.ts
  • apps/posts/src/hooks/use-post-details.ts
  • apps/posts/src/providers/post-analytics-context.tsx
  • apps/posts/src/utils/constants.ts
  • apps/posts/src/utils/gift-link.ts
  • apps/posts/src/views/PostAnalytics/Overview/overview.tsx
  • apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
  • apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
  • apps/posts/test/unit/utils/gift-link.test.ts
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
  • ghost/admin/app/components/posts-list/context-menu.hbs
  • ghost/admin/app/components/posts-list/context-menu.js
  • ghost/admin/app/services/feature.js
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/app/utils/gift-link.js
  • ghost/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

Comment on lines +218 to +225
<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>

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.

📐 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.

Suggested change
<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.

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.

2 participants