From 487bea51b22dc7ea6817807775f4d3004d38d8fa Mon Sep 17 00:00:00 2001 From: Rish Date: Tue, 28 Apr 2020 01:10:08 +0530 Subject: [PATCH] Refactored code structure using react context ref https://github.com/TryGhost/members.js/issues/5 React context allows us cleaner setup in codebase with shared data and methods across components at different nesting levels. This should allow faster iteration and easier development going forward. - Uses Parent context for shared data and methods across different components instead of passed down props - Uses new `init` method in API for data initialization - Removes `PopupMenu` component in favor of `PopupModal` - Adds new test util for custom render for easier base setup - https://testing-library.com/docs/react-testing-library/setup#custom-render - Updates tests to use new test util for easier test setup --- ghost/portal/src/App.test.js | 3 +- .../portal/src/components/ParentContainer.js | 216 ++++++++---------- ghost/portal/src/components/PopupMenu.js | 144 ------------ ghost/portal/src/components/PopupMenu.test.js | 15 -- ghost/portal/src/components/PopupModal.js | 54 ++--- ghost/portal/src/components/TriggerButton.js | 129 ++++++----- .../src/components/pages/AccountHomePage.js | 40 ++-- .../components/pages/AccountHomePage.test.js | 11 +- .../src/components/pages/MagicLinkPage.js | 9 +- .../portal/src/components/pages/SigninPage.js | 28 +-- .../src/components/pages/SigninPage.test.js | 15 +- .../portal/src/components/pages/SignupPage.js | 50 ++-- .../src/components/pages/SignupPage.test.js | 15 +- ghost/portal/src/utils/tests.js | 40 ++++ 14 files changed, 287 insertions(+), 482 deletions(-) delete mode 100644 ghost/portal/src/components/PopupMenu.js delete mode 100644 ghost/portal/src/components/PopupMenu.test.js create mode 100644 ghost/portal/src/utils/tests.js diff --git a/ghost/portal/src/App.test.js b/ghost/portal/src/App.test.js index bf67a2775a..32039b7add 100644 --- a/ghost/portal/src/App.test.js +++ b/ghost/portal/src/App.test.js @@ -1,11 +1,10 @@ import React from 'react'; import {render} from '@testing-library/react'; import App from './App'; -import {site} from './test/fixtures/data'; test('renders App', () => { const {container} = render( - + ); // dashboard component should be rendered on root route diff --git a/ghost/portal/src/components/ParentContainer.js b/ghost/portal/src/components/ParentContainer.js index b48be0fb0e..12f76e532c 100644 --- a/ghost/portal/src/components/ParentContainer.js +++ b/ghost/portal/src/components/ParentContainer.js @@ -1,11 +1,12 @@ import TriggerButton from './TriggerButton'; -import PopupMenu from './PopupMenu'; import PopupModal from './PopupModal'; import * as Fixtures from '../test/fixtures/data'; -import Api from '../utils/api'; +import setupGhostApi from '../utils/api'; +import {ParentContext} from './ParentContext'; const React = require('react'); const PropTypes = require('prop-types'); + export default class ParentContainer extends React.Component { static propTypes = { data: PropTypes.object.isRequired @@ -14,57 +15,64 @@ export default class ParentContainer extends React.Component { constructor(props) { super(props); - this.state = { - page: 'magiclink', - showPopup: false, - action: { - name: 'loading' - } - }; + // Setup custom trigger button handling + this.setupCustomTriggerButton(); - this.initialize(); + this.state = { + page: 'accountHome', + showPopup: false, + action: 'init:running', + initStatus: 'running' + }; } componentDidMount() { - // Initialize site and members data - - this.loadData(); - } - - initialize() { - // Setup custom trigger button handling - this.setupCustomTriggerButton(); - } - - async loadData() { - // Setup Members API with site/admin URLs const {adminUrl} = this.props.data; - const siteUrl = window.location.origin; - this.MembersAPI = Api({siteUrl, adminUrl}); + if (adminUrl) { + this.GhostApi = setupGhostApi({adminUrl}); + this.fetchData(); + } else { + console.error(`[Members.js] Failed to initialize, pass a valid admin url.`); + this.setState({ + action: 'init:failed:missingAdminUrl' + }); + } + } + + // Fetch site and member session data with Ghost Apis + async fetchData() { + const {adminUrl} = this.props.data; + this.GhostApi = setupGhostApi({adminUrl}); try { - const [{site}, member] = await Promise.all([this.MembersAPI.site.read(), this.MembersAPI.member.sessionData()]); - console.log('Initialized Members.js with', site, member); + const {site, member} = await this.GhostApi.init(); this.setState({ site, member, page: member ? 'accountHome' : 'signup', - action: 'init:success' + action: 'init:success', + initStatus: 'success' }); } catch (e) { - console.log('Failed state fetch', e); + console.error(`[Members.js] Failed to fetch site data, please make sure your admin url - ${adminUrl} - is correct.`); this.setState({ - action: { - name: 'init:failed' - } + action: 'init:failed:incorrectAdminUrl', + initStatus: 'failed' }); } } getData() { - const member = process.env.REACT_APP_ADMIN_URL ? Fixtures.member.free : this.state.member; - const site = process.env.REACT_APP_ADMIN_URL ? Fixtures.site : this.state.site; - - return {site, member}; + // Load data from fixtures for development mode + if (process.env.REACT_APP_ADMIN_URL) { + return { + site: Fixtures.site, + member: Fixtures.member.free + }; + } + return { + site: this.state.site, + member: this.state.member + }; } switchPage(page) { @@ -74,7 +82,8 @@ export default class ParentContainer extends React.Component { } setupCustomTriggerButton() { - this.customTriggerButton = document.querySelector('[data-members-trigger-button]'); + const customTriggerSelector = '[data-members-trigger-button]'; + this.customTriggerButton = document.querySelector(customTriggerSelector); if (this.customTriggerButton) { const clickHandler = (event) => { @@ -83,78 +92,56 @@ export default class ParentContainer extends React.Component { const elRemoveClass = this.state.showPopup ? 'popup-open' : 'popup-close'; this.customTriggerButton.classList.add(elAddClass); this.customTriggerButton.classList.remove(elRemoveClass); - this.onTriggerToggle(); + this.onAction('togglePopup'); }; this.customTriggerButton.classList.add('popup-close'); this.customTriggerButton.addEventListener('click', clickHandler); } } - resetAction() { - this.setState({ - action: null - }); - } - getBrandColor() { - return this.getData().site && this.getData().site.brand && this.getData().site.brand.primaryColor; + return (this.getData().site && this.getData().site.brand && this.getData().site.brand.primaryColor) || '#3db0ef'; } async onAction(action, data) { this.setState({ - action: { - name: action, - isRunning: true, - isSuccess: false, - error: null - } + action: `${action}:running` }); try { - if (action === 'closePopup') { + if (action === 'switchPage') { + this.setState({ + page: data + }); + } else if (action === 'togglePopup') { + this.setState({ + showPopup: !this.state.showPopup + }); + } else if (action === 'closePopup') { this.setState({ showPopup: false }); } else if (action === 'signout') { - await this.MembersAPI.member.signout(); - + await this.GhostApi.member.signout(); this.setState({ - action: { - name: action, - isRunning: false, - isSuccess: true - } + action: 'signout:success' }); - } - - if (action === 'signin') { - await this.MembersAPI.member.sendMagicLink(data); + } else if (action === 'signin') { + await this.GhostApi.member.sendMagicLink(data); this.setState({ - action: { - name: action, - isRunning: false, - isSuccess: true - }, + action: 'signin:success', page: 'magiclink' }); - } - - if (action === 'signup') { - await this.MembersAPI.member.sendMagicLink(data); + } else if (action === 'signup') { + await this.GhostApi.member.sendMagicLink(data); this.setState({ - action: { - name: action, - isRunning: false, - isSuccess: true - }, + action: 'signup:success', page: 'magiclink' }); - } - - if (action === 'checkoutPlan') { + } else if (action === 'checkoutPlan') { const checkoutSuccessUrl = (new URL('/account/?stripe=billing-update-success', window.location.href)).href; const checkoutCancelUrl = (new URL('/account/?stripe=billing-update-cancel', window.location.href)).href; const {plan} = data; - await this.MembersAPI.member.checkoutPlan({ + await this.GhostApi.member.checkoutPlan({ plan, checkoutSuccessUrl, checkoutCancelUrl @@ -162,47 +149,15 @@ export default class ParentContainer extends React.Component { } } catch (e) { this.setState({ - action: { - name: action, - isRunning: false, - error: e - } + action: `${action}:failed` }); } } - onTriggerToggle() { - let showPopup = !this.state.showPopup; - this.setState({ - showPopup - }); - } - renderPopupMenu() { if (this.state.showPopup) { - if (this.state.page === 'accountHome') { - return ( - this.onTriggerToggle()} - page={this.state.page} - switchPage={page => this.switchPage(page)} - onAction={(action, data) => this.onAction(action, data)} - brandColor = {this.getBrandColor()} - /> - ); - } return ( - this.onTriggerToggle()} - page={this.state.page} - switchPage={page => this.switchPage(page)} - onAction={(action, data) => this.onAction(action, data)} - brandColor = {this.getBrandColor()} - /> + ); } return null; @@ -212,11 +167,7 @@ export default class ParentContainer extends React.Component { if (!this.customTriggerButton) { return ( this.onTriggerToggle()} isPopupOpen={this.state.showPopup} - data={this.getData()} - brandColor = {this.getBrandColor()} /> ); } @@ -224,12 +175,29 @@ export default class ParentContainer extends React.Component { return null; } + getActionData(action) { + const [type, status, reason] = action.split(':'); + return {type, status, reason}; + } + render() { - return ( - <> - {this.renderPopupMenu()} - {this.renderTriggerButton()} - - ); + if (this.state.initStatus === 'success' || process.env.REACT_APP_ADMIN_URL) { + const {site, member} = this.getData(); + + return ( + this.onAction(action, data) + }}> + {this.renderPopupMenu()} + {this.renderTriggerButton()} + + ); + } + return null; } } diff --git a/ghost/portal/src/components/PopupMenu.js b/ghost/portal/src/components/PopupMenu.js deleted file mode 100644 index 15d2452cdd..0000000000 --- a/ghost/portal/src/components/PopupMenu.js +++ /dev/null @@ -1,144 +0,0 @@ -import Frame from './Frame'; -import SigninPage from './pages/SigninPage'; -import SignupPage from './pages/SignupPage'; -import AccountHomePage from './pages/AccountHomePage'; -import MagicLinkPage from './pages/MagicLinkPage'; -import LoadingPage from './pages/LoadingPage'; - -const React = require('react'); -const PropTypes = require('prop-types'); - -const Styles = { - frame: { - common: { - zIndex: '2147483000', - position: 'fixed', - bottom: '100px', - right: '20px', - width: '350px', - minHeight: '350px', - maxHeight: '410px', - boxShadow: 'rgba(0, 0, 0, 0.16) 0px 5px 40px', - opacity: '1', - height: 'calc(100% - 120px)', - borderRadius: '8px', - overflow: 'hidden', - backgroundColor: 'white' - }, - signin: { - width: '400px', - minHeight: '200px', - maxHeight: '240px' - }, - signup: { - width: '450px', - minHeight: '400px', - maxHeight: '460px' - }, - accountHome: { - width: '280px', - minHeight: '200px', - maxHeight: '240px' - }, - magiclink: { - width: '400px', - minHeight: '130px', - maxHeight: '130px' - }, - loading: { - width: '250px', - minHeight: '130px', - maxHeight: '130px' - } - }, - popup: { - parent: { - width: '100%', - height: '100%', - position: 'absolute', - letterSpacing: '0', - textRendering: 'optimizeLegibility', - fontSize: '1.5rem' - }, - container: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-start', - position: 'absolute', - top: '0px', - bottom: '0px', - left: '0px', - right: '0px', - overflow: 'hidden', - paddingTop: '18px', - paddingBottom: '18px', - textAlign: 'left' - } - } -}; - -const Pages = { - signin: SigninPage, - signup: SignupPage, - accountHome: AccountHomePage, - magiclink: MagicLinkPage, - loading: LoadingPage -}; - -export default class PopupMenu extends React.Component { - static propTypes = { - data: PropTypes.shape({ - site: PropTypes.shape({ - title: PropTypes.string, - description: PropTypes.string - }).isRequired, - member: PropTypes.shape({ - email: PropTypes.string - }) - }).isRequired, - action: PropTypes.object, - page: PropTypes.string.isRequired, - onAction: PropTypes.func.isRequired - }; - - renderCurrentPage(page) { - const PageComponent = Pages[page]; - - return ( - this.props.onAction(action, data)} - switchPage={page => this.props.switchPage(page)} - /> - ); - } - - renderPopupContent() { - return ( -
-
- {this.renderCurrentPage(this.props.page)} -
-
- ); - } - - renderFrameContainer() { - const page = this.props.page; - const frameStyle = { - ...Styles.frame.common, - ...Styles.frame[page] - }; - - return ( - - {this.renderPopupContent()} - - ); - } - - render() { - return this.renderFrameContainer(); - } -} diff --git a/ghost/portal/src/components/PopupMenu.test.js b/ghost/portal/src/components/PopupMenu.test.js deleted file mode 100644 index bbefb7d4f2..0000000000 --- a/ghost/portal/src/components/PopupMenu.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import {render} from '@testing-library/react'; -import PopupMenu from './PopupMenu'; -import {site} from '../test/fixtures/data'; - -describe('Popup Menu', () => { - test('renders', () => { - const {getByTitle} = render( - {}} /> - ); - const popupFrame = getByTitle('membersjs-popup'); - - expect(popupFrame).toBeInTheDocument(); - }); -}); diff --git a/ghost/portal/src/components/PopupModal.js b/ghost/portal/src/components/PopupModal.js index b81cf800c2..3baf49ac39 100644 --- a/ghost/portal/src/components/PopupModal.js +++ b/ghost/portal/src/components/PopupModal.js @@ -5,9 +5,9 @@ import AccountHomePage from './pages/AccountHomePage'; import MagicLinkPage from './pages/MagicLinkPage'; import LoadingPage from './pages/LoadingPage'; import {ReactComponent as CloseIcon} from '../images/icons/close.svg'; +import {ParentContext} from './ParentContext'; const React = require('react'); -const PropTypes = require('prop-types'); const Styles = { modalContainer: { @@ -35,6 +35,19 @@ const Styles = { height: '60%', backgroundColor: 'white' }, + menu: { + position: 'fixed', + padding: '0', + outline: '0', + bottom: '100px', + right: '20px', + borderRadius: '8px', + boxShadow: 'rgba(0, 0, 0, 0.16) 0px 5px 40px', + opacity: '1', + overflow: 'hidden', + height: '60%', + backgroundColor: 'white' + }, signin: { minHeight: '200px', maxHeight: '330px' @@ -44,8 +57,9 @@ const Styles = { maxHeight: '620px' }, accountHome: { - minHeight: '350px', - maxHeight: '510px' + width: '280px', + minHeight: '200px', + maxHeight: '240px' }, magiclink: { minHeight: '230px', @@ -94,39 +108,20 @@ const Pages = { }; export default class PopupModal extends React.Component { - static propTypes = { - data: PropTypes.shape({ - site: PropTypes.shape({ - title: PropTypes.string, - description: PropTypes.string - }).isRequired, - member: PropTypes.shape({ - email: PropTypes.string - }) - }).isRequired, - action: PropTypes.object, - page: PropTypes.string.isRequired, - onAction: PropTypes.func.isRequired - }; + static contextType = ParentContext; renderCurrentPage(page) { const PageComponent = Pages[page]; return ( - this.props.onAction(action, data)} - brandColor={this.props.brandColor} - switchPage={page => this.props.switchPage(page)} - /> + ); } renderPopupClose() { return (
- this.props.onToggle()} /> + this.context.onAction('closePopup')} />
); } @@ -135,7 +130,7 @@ export default class PopupModal extends React.Component { return (
{this.renderPopupClose()} - {this.renderCurrentPage(this.props.page)} + {this.renderCurrentPage(this.context.page)}
); } @@ -143,14 +138,15 @@ export default class PopupModal extends React.Component { handlePopupClose(e) { e.preventDefault(); if (e.target === e.currentTarget) { - this.props.onToggle(); + this.context.onAction('closePopup'); } } renderFrameContainer() { - const page = this.props.page; + const page = this.context.page; + const commonStyle = this.context.page === 'accountHome' ? Styles.frame.menu : Styles.frame.common; const frameStyle = { - ...Styles.frame.common, + ...commonStyle, ...Styles.frame[page] }; return ( diff --git a/ghost/portal/src/components/TriggerButton.js b/ghost/portal/src/components/TriggerButton.js index 6a645b15e4..b5f90063a5 100644 --- a/ghost/portal/src/components/TriggerButton.js +++ b/ghost/portal/src/components/TriggerButton.js @@ -1,94 +1,93 @@ import Frame from './Frame'; +import {ParentContext} from './ParentContext'; import {ReactComponent as UserIcon} from '../images/icons/user.svg'; import {ReactComponent as CloseIcon} from '../images/icons/close.svg'; const React = require('react'); -const PropTypes = require('prop-types'); -const Styles = { - frame: { - zIndex: '2147483000', - position: 'fixed', - bottom: '20px', - right: '20px', - width: '60px', - height: '60px', - boxShadow: 'rgba(0, 0, 0, 0.06) 0px 1px 6px 0px, rgba(0, 0, 0, 0.16) 0px 2px 32px 0px', - borderRadius: '50%', - backgroundColor: '#3EB0EF', - animation: '250ms ease 0s 1 normal none running animation-bhegco', - transition: 'opacity 0.3s ease 0s' - }, - launcher: { - position: 'absolute', - top: '0px', - left: '0px', - width: '60px', - height: '60px', - cursor: 'pointer', - transformOrigin: 'center center', - backfaceVisibility: 'hidden', - WebkitFontSmoothing: 'antialiased', - borderRadius: '50%', - overflow: 'hidden' - }, - button: { - display: 'flex', - WebkitBoxAlign: 'center', - alignItems: 'center', - WebkitBoxPack: 'center', - justifyContent: 'center', - position: 'absolute', - top: '0px', - bottom: '0px', - width: '100%', - opacity: '1', - transform: 'rotate(0deg) scale(1)', - transition: 'transform 0.16s linear 0s, opacity 0.08s linear 0s' - }, - userIcon: { - width: '20px', - height: '20px', - color: '#fff' - }, +const Styles = ({brandColor}) => { + return { + frame: { + zIndex: '2147483000', + position: 'fixed', + bottom: '20px', + right: '20px', + width: '60px', + height: '60px', + boxShadow: 'rgba(0, 0, 0, 0.06) 0px 1px 6px 0px, rgba(0, 0, 0, 0.16) 0px 2px 32px 0px', + borderRadius: '50%', + backgroundColor: brandColor, + animation: '250ms ease 0s 1 normal none running animation-bhegco', + transition: 'opacity 0.3s ease 0s' + }, + launcher: { + position: 'absolute', + top: '0px', + left: '0px', + width: '60px', + height: '60px', + cursor: 'pointer', + transformOrigin: 'center center', + backfaceVisibility: 'hidden', + WebkitFontSmoothing: 'antialiased', + borderRadius: '50%', + overflow: 'hidden' + }, + button: { + display: 'flex', + WebkitBoxAlign: 'center', + alignItems: 'center', + WebkitBoxPack: 'center', + justifyContent: 'center', + position: 'absolute', + top: '0px', + bottom: '0px', + width: '100%', + opacity: '1', + transform: 'rotate(0deg) scale(1)', + transition: 'transform 0.16s linear 0s, opacity 0.08s linear 0s' + }, + userIcon: { + width: '20px', + height: '20px', + color: '#fff' + }, - closeIcon: { - width: '20px', - height: '20px', - color: '#fff' - } + closeIcon: { + width: '20px', + height: '20px', + color: '#fff' + } + }; }; export default class TriggerButton extends React.Component { - static propTypes = { - name: PropTypes.string - }; + static contextType = ParentContext; onToggle() { - this.props.onToggle(); + this.context.onAction('togglePopup'); } renderTriggerIcon() { + const Style = Styles({brandColor: this.context.brandColor}); + if (this.props.isPopupOpen) { return ( - + ); } return ( - + ); } render() { - const frameStyle = { - ...Styles.frame, - backgroundColor: this.props.brandColor || '#3EB0EF' - }; + const Style = Styles({brandColor: this.context.brandColor}); return ( - -
this.onToggle(e)}> -
+ +
this.onToggle(e)}> +
{this.renderTriggerIcon()}
diff --git a/ghost/portal/src/components/pages/AccountHomePage.js b/ghost/portal/src/components/pages/AccountHomePage.js index b0762dbd6f..0fe227a7bd 100644 --- a/ghost/portal/src/components/pages/AccountHomePage.js +++ b/ghost/portal/src/components/pages/AccountHomePage.js @@ -1,34 +1,20 @@ +import {ParentContext} from '../ParentContext'; + const React = require('react'); -const PropTypes = require('prop-types'); export default class AccountHomePage extends React.Component { - static propTypes = { - data: PropTypes.shape({ - site: PropTypes.shape({ - title: PropTypes.string, - description: PropTypes.string - }).isRequired, - member: PropTypes.shape({ - email: PropTypes.string - }).isRequired - }).isRequired, - onAction: PropTypes.func - }; + static contextType = ParentContext; handleSignout(e) { e.preventDefault(); - this.props.onAction('signout'); + this.context.onAction('signout'); } handlePlanCheckout(e) { e.preventDefault(); const plan = e.target.name; - const email = this.getMemberEmail(); - this.props.onAction('checkoutPlan', {email, plan}); - } - - getMemberEmail() { - return this.props.data.member.email; + const email = this.context.member.email; + this.context.onAction('checkoutPlan', {email, plan}); } renderPlanSelectButton({name}) { @@ -45,7 +31,7 @@ export default class AccountHomePage extends React.Component { cursor: 'pointer', transition: '.4s ease', color: '#fff', - backgroundColor: this.props.brandColor || '#3eb0ef', + backgroundColor: this.context.brandColor, boxShadow: 'none', userSelect: 'none', width: '90px', @@ -110,8 +96,8 @@ export default class AccountHomePage extends React.Component { border: '1px solid black', marginBottom: '12px' }; - const siteTitle = this.props.data.site && this.props.data.site.title; - const plans = this.props.data.site && this.props.data.site.plans; + const siteTitle = this.context.site.title; + const plans = this.context.site.plans; return (
@@ -127,7 +113,7 @@ export default class AccountHomePage extends React.Component { } renderHeader() { - const memberEmail = this.getMemberEmail(); + const memberEmail = this.context.member.email; return ( <> @@ -142,7 +128,7 @@ export default class AccountHomePage extends React.Component { } renderUserAvatar() { - const avatarImg = (this.props.data.member && this.props.data.member.avatar_image); + const avatarImg = (this.context.member && this.context.member.avatar_image); const logoStyle = { position: 'relative', @@ -167,8 +153,8 @@ export default class AccountHomePage extends React.Component { } renderUserHeader() { - const memberEmail = this.getMemberEmail(); - const memberName = this.props.data.member.name; + const memberEmail = this.context.member.email; + const memberName = this.context.member.name; return (
diff --git a/ghost/portal/src/components/pages/AccountHomePage.test.js b/ghost/portal/src/components/pages/AccountHomePage.test.js index 266eb177f2..d59f43e8e7 100644 --- a/ghost/portal/src/components/pages/AccountHomePage.test.js +++ b/ghost/portal/src/components/pages/AccountHomePage.test.js @@ -1,14 +1,12 @@ import React from 'react'; -import {render, fireEvent} from '@testing-library/react'; +import {render, fireEvent} from '../../utils/tests'; import AccountHomePage from './AccountHomePage'; -import {site, member} from '../../test/fixtures/data'; +import {member} from '../../test/fixtures/data'; const setup = (overrides) => { - const mockOnActionFn = jest.fn(); - const mockSwitchPageFn = jest.fn(); const freeMember = member.free; - const utils = render( - + const {mockOnActionFn, ...utils} = render( + ); const memberEmail = utils.getByText(freeMember.email); const logoutButton = utils.queryByRole('button', {name: 'Log out'}); @@ -16,7 +14,6 @@ const setup = (overrides) => { memberEmail, logoutButton, mockOnActionFn, - mockSwitchPageFn, ...utils }; }; diff --git a/ghost/portal/src/components/pages/MagicLinkPage.js b/ghost/portal/src/components/pages/MagicLinkPage.js index 2e525fd250..08c4e60b04 100644 --- a/ghost/portal/src/components/pages/MagicLinkPage.js +++ b/ghost/portal/src/components/pages/MagicLinkPage.js @@ -1,7 +1,10 @@ import ActionButton from '../common/ActionButton'; +import {ParentContext} from '../ParentContext'; const React = require('react'); export default class MagicLinkPage extends React.Component { + static contextType = ParentContext; + renderFormHeader() { return (
@@ -14,13 +17,13 @@ export default class MagicLinkPage extends React.Component { renderLoginMessage() { return (
-
this.props.switchPage('signin')}> Back to Log in
+
this.context.onAction('switchPage', 'signin')}> Back to Log in
); } handleClose(e) { - this.props.onAction('closePopup'); + this.context.onAction('closePopup'); } renderCloseButton() { @@ -28,7 +31,7 @@ export default class MagicLinkPage extends React.Component { return ( this.handleSignin(e)} - brandColor={this.props.brandColor} + brandColor={this.context.brandColor} label={label} /> ); diff --git a/ghost/portal/src/components/pages/SigninPage.js b/ghost/portal/src/components/pages/SigninPage.js index 9aecf06c50..5947715f38 100644 --- a/ghost/portal/src/components/pages/SigninPage.js +++ b/ghost/portal/src/components/pages/SigninPage.js @@ -1,19 +1,11 @@ import ActionButton from '../common/ActionButton'; import InputField from '../common/InputField'; +import {ParentContext} from '../ParentContext'; const React = require('react'); -const PropTypes = require('prop-types'); export default class SigninPage extends React.Component { - static propTypes = { - data: PropTypes.shape({ - site: PropTypes.shape({ - title: PropTypes.string, - description: PropTypes.string - }).isRequired - }).isRequired, - onAction: PropTypes.func.isRequired - }; + static contextType = ParentContext; constructor(props) { super(props); @@ -26,7 +18,7 @@ export default class SigninPage extends React.Component { e.preventDefault(); const email = this.state.email; - this.props.onAction('signin', {email}); + this.context.onAction('signin', {email}); } handleInput(e, field) { @@ -38,14 +30,14 @@ export default class SigninPage extends React.Component { } renderSubmitButton() { - const isRunning = this.props.action && this.props.action.name === 'signin' && this.props.action.isRunning; - const label = this.state.isLoading ? 'Sending' : 'Send Login Link'; + const isRunning = (this.context.action === 'signin:running'); + const label = isRunning ? 'Sending' : 'Send Login Link'; const disabled = isRunning ? true : false; return ( this.handleSignin(e)} disabled={disabled} - brandColor={this.props.brandColor} + brandColor={this.context.brandColor} label={label} /> ); @@ -75,11 +67,11 @@ export default class SigninPage extends React.Component { } renderSignupMessage() { - const color = this.props.brandColor || '#3db0ef'; + const brandColor = this.context.brandColor; return (
Don't have an account ?
-
this.props.switchPage('signup')}> Subscribe
+
this.context.onAction('switchPage', 'signup')}> Subscribe
); } @@ -95,7 +87,7 @@ export default class SigninPage extends React.Component { } renderSiteLogo() { - const siteLogo = (this.props.data.site && this.props.data.site.logo); + const siteLogo = this.context.site.logo; const logoStyle = { position: 'relative', @@ -119,7 +111,7 @@ export default class SigninPage extends React.Component { } renderFormHeader() { - const siteTitle = (this.props.data.site && this.props.data.site.title) || 'Site Title'; + const siteTitle = this.context.site.title || 'Site Title'; return (
diff --git a/ghost/portal/src/components/pages/SigninPage.test.js b/ghost/portal/src/components/pages/SigninPage.test.js index 0575c31bcd..9e890105d4 100644 --- a/ghost/portal/src/components/pages/SigninPage.test.js +++ b/ghost/portal/src/components/pages/SigninPage.test.js @@ -1,14 +1,10 @@ import React from 'react'; -import {render, fireEvent} from '@testing-library/react'; +import {render, fireEvent} from '../../utils/tests'; import SigninPage from './SigninPage'; -import {site} from '../../test/fixtures/data'; const setup = (overrides) => { - const mockOnActionFn = jest.fn(); - const mockSwitchPageFn = jest.fn(); - - const utils = render( - + const {mockOnActionFn, ...utils} = render( + ); const emailInput = utils.getByLabelText(/email/i); const submitButton = utils.queryByRole('button', {name: 'Send Login Link'}); @@ -18,7 +14,6 @@ const setup = (overrides) => { submitButton, signupButton, mockOnActionFn, - mockSwitchPageFn, ...utils }; }; @@ -43,9 +38,9 @@ describe('SigninPage', () => { }); test('can call swithPage for signup', () => { - const {signupButton, mockSwitchPageFn} = setup(); + const {signupButton, mockOnActionFn} = setup(); fireEvent.click(signupButton); - expect(mockSwitchPageFn).toHaveBeenCalledWith('signup'); + expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', 'signup'); }); }); diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index caba096eac..7e3d75d6bf 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -1,57 +1,46 @@ import ActionButton from '../common/ActionButton'; import InputField from '../common/InputField'; +import {ParentContext} from '../ParentContext'; const React = require('react'); -const PropTypes = require('prop-types'); -export default class SignupPage extends React.Component { - static propTypes = { - data: PropTypes.shape({ - site: PropTypes.shape({ - title: PropTypes.string, - description: PropTypes.string - }).isRequired - }).isRequired, - onAction: PropTypes.func.isRequired, - switchPage: PropTypes.func.isRequired - }; +class SignupPage extends React.Component { + static contextType = ParentContext; constructor(props) { super(props); this.state = { name: '', email: '', - plan: 'FREE', - isLoading: false, - showSuccess: false + plan: 'FREE' }; } handleSignup(e) { e.preventDefault(); + const {onAction} = this.context; const email = this.state.email; const name = this.state.name; const plan = this.state.plan; - this.props.onAction('signup', {name, email, plan}); + onAction('signup', {name, email, plan}); } handleInput(e, field) { this.setState({ - [field]: e.target.value, - showSuccess: false, - isLoading: false + [field]: e.target.value }); } renderSubmitButton() { - const isRunning = this.props.action && this.props.action.name === 'signup' && this.props.action.isRunning; - const label = this.state.isLoading ? 'Sending...' : 'Continue'; - const disabled = isRunning ? true : false; + const {action, brandColor} = this.context; + + const label = (action === 'signup:running') ? 'Sending...' : 'Continue'; + const disabled = (action === 'signup:running') ? true : false; return ( this.handleSignup(e)} disabled={disabled} - brandColor={this.props.brandColor} + brandColor={brandColor} label={label} /> ); @@ -161,7 +150,8 @@ export default class SignupPage extends React.Component { borderRadius: '9px', marginBottom: '12px' }; - const plans = this.props.data.site && this.props.data.site.plans; + let {site} = this.context; + const plans = site.plans; if (!plans) { return null; } @@ -210,11 +200,11 @@ export default class SignupPage extends React.Component { } renderLoginMessage() { - const color = this.props.brandColor || '#3db0ef'; + const {brandColor, onAction} = this.context; return (
Already a member ?
-
this.props.switchPage('signin')}> Log in
+
onAction('switchPage', 'signin')}> Log in
); } @@ -232,7 +222,8 @@ export default class SignupPage extends React.Component { } renderSiteLogo() { - const siteLogo = (this.props.data.site && this.props.data.site.logo); + const {site} = this.context; + const siteLogo = site.logo; const logoStyle = { position: 'relative', @@ -256,7 +247,8 @@ export default class SignupPage extends React.Component { } renderFormHeader() { - const siteTitle = (this.props.data.site && this.props.data.site.title) || 'Site Title'; + const {site} = this.context; + const siteTitle = site.title || 'Site Title'; return (
@@ -277,3 +269,5 @@ export default class SignupPage extends React.Component { ); } } + +export default SignupPage; \ No newline at end of file diff --git a/ghost/portal/src/components/pages/SignupPage.test.js b/ghost/portal/src/components/pages/SignupPage.test.js index 1b1884c3c6..854ef17ef0 100644 --- a/ghost/portal/src/components/pages/SignupPage.test.js +++ b/ghost/portal/src/components/pages/SignupPage.test.js @@ -1,14 +1,10 @@ import React from 'react'; -import {render, fireEvent} from '@testing-library/react'; import SignupPage from './SignupPage'; -import {site} from '../../test/fixtures/data'; +import {render, fireEvent} from '../../utils/tests'; const setup = (overrides) => { - const mockOnActionFn = jest.fn(); - const mockSwitchPageFn = jest.fn(); - - const utils = render( - + const {mockOnActionFn, ...utils} = render( + ); const emailInput = utils.getByLabelText(/email/i); const nameInput = utils.getByLabelText(/name/i); @@ -20,7 +16,6 @@ const setup = (overrides) => { submitButton, signinButton, mockOnActionFn, - mockSwitchPageFn, ...utils }; }; @@ -51,9 +46,9 @@ describe('SignupPage', () => { }); test('can call swithPage for signin', () => { - const {signinButton, mockSwitchPageFn} = setup(); + const {signinButton, mockOnActionFn} = setup(); fireEvent.click(signinButton); - expect(mockSwitchPageFn).toHaveBeenCalledWith('signin'); + expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', 'signin'); }); }); diff --git a/ghost/portal/src/utils/tests.js b/ghost/portal/src/utils/tests.js new file mode 100644 index 0000000000..d284154656 --- /dev/null +++ b/ghost/portal/src/utils/tests.js @@ -0,0 +1,40 @@ +// Common test setup util - Ref: https://testing-library.com/docs/react-testing-library/setup#custom-render +import React from 'react'; +import {render} from '@testing-library/react'; +import {ParentContext} from '../components/ParentContext'; +import {site, member} from '../test/fixtures/data'; + +const setupProvider = (context) => { + return ({children}) => { + return ( + + {children} + + ); + }; +}; + +const customRender = (ui, {options = {}, overrideContext = {}} = {}) => { + const mockOnActionFn = jest.fn(); + + const context = { + site, + member: member.free, + action: 'init:success', + brandColor: site.brand.primaryColor, + page: 'signup', + onAction: mockOnActionFn, + ...overrideContext + }; + const utils = render(ui, {wrapper: setupProvider(context), ...options}); + return { + ...utils, + mockOnActionFn + }; +}; + +// re-export everything +export * from '@testing-library/react'; + +// override render method +export {customRender as render}; \ No newline at end of file