mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -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 PopupModal from './components/PopupModal';
|
||||||
import setupGhostApi from './utils/api';
|
import setupGhostApi from './utils/api';
|
||||||
import AppContext from './AppContext';
|
import AppContext from './AppContext';
|
||||||
import {hasMode} from './utils/check-mode';
|
import NotificationParser from './utils/notifications';
|
||||||
import * as Fixtures from './utils/fixtures';
|
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 {getActivePage, isAccountPage, isOfferPage} from './pages';
|
||||||
import ActionHandler from './actions';
|
import ActionHandler from './actions';
|
||||||
import './App.css';
|
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 {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';
|
import {handleDataAttributes} from './data-attributes';
|
||||||
|
|
||||||
|
@ -184,10 +185,9 @@ export default class App extends React.Component {
|
||||||
};
|
};
|
||||||
window.addEventListener('hashchange', this.hashHandler, false);
|
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) {
|
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]');
|
const formElements = document.querySelectorAll('[data-lexical-signup-form]');
|
||||||
if (formElements.length > 0){
|
if (formElements.length > 0){
|
||||||
formElements.forEach((element) => {
|
formElements.forEach((element) => {
|
||||||
|
@ -195,7 +195,11 @@ export default class App extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupRecommendationButtons();
|
this.setupRecommendationButtons();
|
||||||
|
|
||||||
|
// avoid portal links switching to homepage (e.g. from absolute link copy/pasted from Admin)
|
||||||
|
this.transformPortalLinksToRelative();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.error(`[Portal] Failed to initialize:`, e);
|
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() {
|
render() {
|
||||||
if (this.state.initStatus === 'success') {
|
if (this.state.initStatus === 'success') {
|
||||||
return (
|
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