Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions ghost/admin/app/routes/lexical-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,97 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';

const POSTS_ANALYTICS_ROUTE = 'posts-x';
const STATS_ANALYTICS_ROUTE = 'stats-x';

/**
* Checks whether a route name belongs to a route family.
* @param {string|undefined|null} routeName - Current route name.
* @param {string} routeFamily - Base route family name.
* @returns {boolean}
*/
function isInRouteFamily(routeName, routeFamily) {
return routeName === routeFamily || routeName?.startsWith(`${routeFamily}.`);
}

/**
* Reads a dynamic segment from the previous route info in a transition.
* @param {object|undefined} transition - Ember transition object.
* @param {string} paramName - Dynamic segment name to resolve.
* @returns {string|undefined}
*/
function getTransitionParam(transition, paramName) {
let routeInfo = transition?.from;

if (!routeInfo) {
return undefined;
}

return routeInfo.params?.[paramName] ?? routeInfo.parent?.params?.[paramName];
}

/**
* Builds a query string from transition query params, excluding empty values.
* @param {Record<string, unknown>} [queryParams={}] - Query params from route info.
* @returns {string} Query string including the leading `?`, or an empty string.
*/
function buildQueryString(queryParams = {}) {
let searchParams = new URLSearchParams();

Object.entries(queryParams).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') {
return;
}

if (Array.isArray(value)) {
value.forEach((item) => {
searchParams.append(key, `${item}`);
});
return;
}

searchParams.append(key, `${value}`);
});

let queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}

/**
* Creates a canonical analytics path to return to from the lexical editor.
* @param {object|undefined} transition - Transition that opened the editor.
* @param {{id?: string|number}|undefined} model - Current post model fallback.
* @returns {string|false} Analytics path when available, otherwise `false`.
*/
function buildAnalyticsSourcePath(transition, model) {
let fromRouteName = transition?.from?.name;
let queryString = buildQueryString(transition?.from?.queryParams);

if (isInRouteFamily(fromRouteName, POSTS_ANALYTICS_ROUTE)) {
let postId = getTransitionParam(transition, 'post_id') || model?.id;

if (!postId) {
return false;
}

let sub = getTransitionParam(transition, 'sub');
let basePath = sub
? `/posts/analytics/${postId}/${sub}`
: `/posts/analytics/${postId}`;

return `${basePath}${queryString}`;
}

if (isInRouteFamily(fromRouteName, STATS_ANALYTICS_ROUTE)) {
let sub = getTransitionParam(transition, 'sub');
let basePath = sub ? `/analytics/${sub}` : '/analytics';

return `${basePath}${queryString}`;
}

return false;
}

export default AuthenticatedRoute.extend({
feature: service(),
notifications: service(),
Expand All @@ -17,14 +108,11 @@ export default AuthenticatedRoute.extend({
},

setupController(controller, model, transition) {
if (transition.from?.name?.startsWith('posts-x') && transition.to?.name !== 'lexical-editor.new') {
// Extract the analytics path from window.location.href to preserve the exact tab
let currentUrl = window.location.href;
// Convert editor URL back to analytics URL and extract just the hash portion
let analyticsUrl = currentUrl.replace('/editor/', '/').replace(/\/edit$/, '');
let hashMatch = analyticsUrl.match(/#(.+)/);
controller.fromAnalytics = hashMatch ? hashMatch[1] : 'posts-x';
if (transition.to?.name === 'lexical-editor.new') {
return;
}

controller.fromAnalytics = buildAnalyticsSourcePath(transition, model) || false;
},

resetController(controller) {
Expand Down
76 changes: 74 additions & 2 deletions ghost/admin/tests/acceptance/editor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ describe('Acceptance: Editor', function () {
).to.equal('/ghost/posts');
});

it('renders a breadcrumb back to the analytics list if that\'s where we came from ', async function () {
it('renders a breadcrumb back to post analytics root if that\'s where we came from', async function () {
let post = this.server.create('post', {
authors: [author],
status: 'published',
Expand All @@ -620,7 +620,79 @@ describe('Acceptance: Editor', function () {
expect(
find('[data-test-breadcrumb]').getAttribute('href'),
'breadcrumb link'
).to.equal(`#posts-x`);
).to.equal(`#/posts/analytics/${post.id}`);
});

it('renders a breadcrumb back to the post analytics subpage if that\'s where we came from', async function () {
let post = this.server.create('post', {
authors: [author],
status: 'published',
title: 'Published Post'
});

await visit(`/posts/analytics/${post.id}/web`);
await visit(`/editor/post/${post.id}`);

expect(
find('[data-test-breadcrumb]').textContent.trim(),
'breadcrumb text'
).to.contain('Analytics');

expect(
find('[data-test-breadcrumb]').getAttribute('href'),
'breadcrumb link'
).to.equal(`#/posts/analytics/${post.id}/web`);
});

it('renders a breadcrumb back to stats root if that\'s where we came from', async function () {
let post = this.server.create('post', {authors: [author]});

await visit('/analytics');
await visit(`/editor/post/${post.id}`);

expect(
find('[data-test-breadcrumb]').textContent.trim(),
'breadcrumb text'
).to.contain('Analytics');

expect(
find('[data-test-breadcrumb]').getAttribute('href'),
'breadcrumb link'
).to.equal('#/analytics');
});

it('renders a breadcrumb back to the stats subpage if that\'s where we came from', async function () {
let post = this.server.create('post', {authors: [author]});

await visit('/analytics/web');
await visit(`/editor/post/${post.id}`);

expect(
find('[data-test-breadcrumb]').textContent.trim(),
'breadcrumb text'
).to.contain('Analytics');

expect(
find('[data-test-breadcrumb]').getAttribute('href'),
'breadcrumb link'
).to.equal('#/analytics/web');
});

it('renders a breadcrumb with stats query params if that\'s where we came from', async function () {
let post = this.server.create('post', {authors: [author]});

await visit('/analytics/growth?tab=total-members');
await visit(`/editor/post/${post.id}`);

expect(
find('[data-test-breadcrumb]').textContent.trim(),
'breadcrumb text'
).to.contain('Analytics');

expect(
find('[data-test-breadcrumb]').getAttribute('href'),
'breadcrumb link'
).to.equal('#/analytics/growth?tab=total-members');
});

it('does not render analytics breadcrumb for a new post', async function () {
Expand Down
Loading