mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
✨ Convert portal links to relative to avoid homepage flash on click
closes https://linear.app/tryghost/issue/PLG-190 - often when adding portal links to your own site pages the URLs are added as absolute on the site's homepage due to copy+paste from displayed URLs in Admin - when clicking absolute portal URLs the homepage is first loaded before the Portal popup is shown resulting in a slower and flashier experience - added a transform for all local portal URLs on the page when Portal is initialized so links open the Portal popup immediately on the current page
This commit is contained in:
parent
a3e498106d
commit
06058db6ae
4 changed files with 112 additions and 5 deletions
|
@ -5,12 +5,13 @@ import Notification from './components/Notification';
|
|||
import PopupModal from './components/PopupModal';
|
||||
import setupGhostApi from './utils/api';
|
||||
import AppContext from './AppContext';
|
||||
import {hasMode} from './utils/check-mode';
|
||||
import NotificationParser from './utils/notifications';
|
||||
import * as Fixtures from './utils/fixtures';
|
||||
import {hasMode} from './utils/check-mode';
|
||||
import {transformPortalAnchorToRelative} from './utils/transform-portal-anchor-to-relative';
|
||||
import {getActivePage, isAccountPage, isOfferPage} from './pages';
|
||||
import ActionHandler from './actions';
|
||||
import './App.css';
|
||||
import NotificationParser from './utils/notifications';
|
||||
import {hasRecommendations, allowCompMemberUpgrade, createPopupNotification, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isComplimentaryMember, isInviteOnlySite, isPaidMember, isRecentMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers';
|
||||
import {handleDataAttributes} from './data-attributes';
|
||||
|
||||
|
@ -184,10 +185,9 @@ export default class App extends React.Component {
|
|||
};
|
||||
window.addEventListener('hashchange', this.hashHandler, false);
|
||||
|
||||
// spike ship - to test if we can show / hide signup forms inside post / page
|
||||
// the signup card will ship hidden by default,
|
||||
// so we need to show it if the member is not logged in
|
||||
if (!member) {
|
||||
// the signup card will ship hidden by default, so we need to show it if the user is not logged in
|
||||
// not sure why a user would have more than one form on a post, but just in case we'll find them all
|
||||
const formElements = document.querySelectorAll('[data-lexical-signup-form]');
|
||||
if (formElements.length > 0){
|
||||
formElements.forEach((element) => {
|
||||
|
@ -195,7 +195,11 @@ export default class App extends React.Component {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setupRecommendationButtons();
|
||||
|
||||
// avoid portal links switching to homepage (e.g. from absolute link copy/pasted from Admin)
|
||||
this.transformPortalLinksToRelative();
|
||||
} catch (e) {
|
||||
/* eslint-disable no-console */
|
||||
console.error(`[Portal] Failed to initialize:`, e);
|
||||
|
@ -956,6 +960,16 @@ export default class App extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform any portal links to use relative paths
|
||||
*
|
||||
* Prevents unwanted/unnecessary switches to the home page when opening the
|
||||
* portal. Especially useful for copy/pasted links from Admin screens.
|
||||
*/
|
||||
transformPortalLinksToRelative() {
|
||||
document.querySelectorAll('a[href*="#/portal"]').forEach(transformPortalAnchorToRelative);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.initStatus === 'success') {
|
||||
return (
|
||||
|
|
34
apps/portal/src/tests/App.test.js
Normal file
34
apps/portal/src/tests/App.test.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import App from '../App';
|
||||
import setupGhostApi from '../utils/api';
|
||||
import {appRender} from '../utils/test-utils';
|
||||
import {site as FixtureSite, member as FixtureMember} from '../utils/test-fixtures';
|
||||
|
||||
describe('App', function () {
|
||||
beforeEach(function () {
|
||||
// Stub window.location with a URL object so we have an expected origin
|
||||
const location = new URL('http://example.com');
|
||||
delete window.location;
|
||||
window.location = location;
|
||||
});
|
||||
|
||||
test('transforms portal links on render', async () => {
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', 'http://example.com/#/portal/signup');
|
||||
document.body.appendChild(link);
|
||||
|
||||
const ghostApi = setupGhostApi({siteUrl: 'http://example.com'});
|
||||
ghostApi.init = jest.fn(() => {
|
||||
return Promise.resolve({
|
||||
site: FixtureSite.singleTier.basic,
|
||||
member: FixtureMember.free
|
||||
});
|
||||
});
|
||||
const utils = appRender(
|
||||
<App api={ghostApi} />
|
||||
);
|
||||
|
||||
await utils.findByTitle(/portal-popup/i);
|
||||
|
||||
expect(link.getAttribute('href')).toBe('#/portal/signup');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import {transformPortalAnchorToRelative} from '../../utils/transform-portal-anchor-to-relative';
|
||||
|
||||
// NOTE: window.location.origin = http://localhost:3000
|
||||
|
||||
describe('transformPortalAnchorToRelative', function () {
|
||||
test('ignores non-portal links', function () {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.setAttribute('href', 'http://localhost:3000/#/signup');
|
||||
transformPortalAnchorToRelative(anchor);
|
||||
|
||||
expect(anchor.getAttribute('href')).toBe('http://localhost:3000/#/signup');
|
||||
});
|
||||
|
||||
test('ignores already-relative links', function () {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.setAttribute('href', '#/portal/signup');
|
||||
transformPortalAnchorToRelative(anchor);
|
||||
|
||||
expect(anchor.getAttribute('href')).toBe('#/portal/signup');
|
||||
});
|
||||
|
||||
test('ignores external links', function () {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.setAttribute('href', 'https://example.com/#/portal/signup');
|
||||
transformPortalAnchorToRelative(anchor);
|
||||
|
||||
expect(anchor.getAttribute('href')).toBe('https://example.com/#/portal/signup');
|
||||
});
|
||||
|
||||
test('converts absolute to a relative link', function () {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.setAttribute('href', 'http://localhost:3000/#/portal/signup');
|
||||
transformPortalAnchorToRelative(anchor);
|
||||
|
||||
expect(anchor.getAttribute('href')).toBe('#/portal/signup');
|
||||
});
|
||||
});
|
22
apps/portal/src/utils/transform-portal-anchor-to-relative.js
Normal file
22
apps/portal/src/utils/transform-portal-anchor-to-relative.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
export function transformPortalAnchorToRelative(anchor) {
|
||||
const href = anchor.getAttribute('href');
|
||||
const url = new URL(href, window.location.origin);
|
||||
|
||||
// ignore non-portal links
|
||||
if (!url.hash || !url.hash.startsWith('#/portal')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore already-relative links
|
||||
if (href.startsWith('#/portal')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore external links
|
||||
if (url.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// convert to a relative link
|
||||
anchor.setAttribute('href', url.hash);
|
||||
}
|
Loading…
Add table
Reference in a new issue