0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Updated notification handling

refs https://github.com/TryGhost/Team/issues/393

- Updated popup notification handling flow for action success and errors
- Updated global notification handling with show popup
- Updated action handler to create popup notifications
- Removed inline newsletter update message
- Generic cleanup of unused code
This commit is contained in:
Rish 2020-09-23 20:33:29 +05:30
parent 8ea26be5bb
commit 5cb1f02109
8 changed files with 238 additions and 56 deletions

View file

@ -328,7 +328,7 @@ export default class App extends React.Component {
/**Get final App level context from data/state*/
getContextFromState() {
const {site, member, action, page, lastPage, showPopup} = this.state;
const {site, member, action, page, lastPage, showPopup, popupNotification} = this.state;
const contextPage = this.getContextPage({page, member});
const contextMember = this.getContextMember({page: contextPage, member});
return {
@ -339,6 +339,7 @@ export default class App extends React.Component {
member: contextMember,
lastPage,
showPopup,
popupNotification,
onAction: (_action, data) => this.onAction(_action, data)
};
}

View file

@ -1,3 +1,17 @@
function createPopupNotification({type, status, autoHide, closeable, state}) {
let count = 0;
if (state.popupNotification) {
count = (state.popupNotification.count || 0) + 1;
}
return {
type,
status,
autoHide,
closeable,
count
};
}
function switchPage({data}) {
return {
page: data.page,
@ -32,6 +46,7 @@ function closePopup({state}) {
return {
showPopup: false,
lastPage: null,
popupNotification: null,
page: state.page === 'magiclink' ? '' : state.page
};
}
@ -57,29 +72,71 @@ async function signout({api}) {
}
async function signin({data, api}) {
await api.member.sendMagicLink(data);
return {
page: 'magiclink'
};
try {
await api.member.sendMagicLink(data);
return {
page: 'magiclink'
};
} catch (e) {
return {
popupNotification: createPopupNotification({
type: 'signin:failed',
autoHide: false,
closeable: true,
status: 'error',
meta: {
reason: e.message
}
})
};
}
}
async function signup({data, api}) {
const {plan, email, name} = data;
if (plan.toLowerCase() === 'free') {
await api.member.sendMagicLink(data);
} else {
await api.member.checkoutPlan({plan, email, name});
try {
const {plan, email, name} = data;
if (plan.toLowerCase() === 'free') {
await api.member.sendMagicLink(data);
} else {
await api.member.checkoutPlan({plan, email, name});
}
return {
page: 'magiclink'
};
} catch (e) {
return {
popupNotification: createPopupNotification({
type: 'signup:failed',
autoHide: false,
closeable: true,
status: 'error',
meta: {
reason: e.message
}
})
};
}
return {
page: 'magiclink'
};
}
async function updateEmail({data, api}) {
await api.member.sendMagicLink(data);
return {
action: 'updateEmail:success'
};
try {
await api.member.sendMagicLink(data);
return {
action: 'updateEmail:success'
};
} catch (e) {
return {
popupNotification: createPopupNotification({
type: 'updateEmail:failed',
autoHide: false,
closeable: true,
status: 'error',
meta: {
reason: e.message
}
})
};
}
}
async function checkoutPlan({data, api}) {
@ -89,14 +146,21 @@ async function checkoutPlan({data, api}) {
});
}
async function updateSubscription({data, api}) {
async function updateSubscription({data, state, api}) {
const {plan, subscriptionId, cancelAtPeriodEnd} = data;
await api.member.updateSubscription({
planName: plan, subscriptionId, cancelAtPeriodEnd
});
const member = await api.member.sessionData();
const action = 'updateSubscription:success';
return {
action: 'updateSubscription:success',
action,
popupNotification: createPopupNotification({
type: action,
autoHide: true,
closeable: true,
state
}),
page: 'accountHome',
member: member
};
@ -119,22 +183,13 @@ async function editBilling({data, api}) {
await api.member.editBilling();
}
async function updateMember({data, api}) {
const {name, subscribed} = data;
const member = await api.member.update({name, subscribed});
if (!member) {
return {
action: 'updateMember:failed'
};
} else {
return {
action: 'updateMember:success',
member: member
};
}
async function clearPopupNotification() {
return {
popupNotification: null
};
}
async function updateNewsletter({data, api}) {
async function updateNewsletter({data, state, api}) {
const {subscribed} = data;
const member = await api.member.update({subscribed});
if (!member) {
@ -142,25 +197,48 @@ async function updateNewsletter({data, api}) {
action: 'updateNewsletter:failed'
};
} else {
const action = 'updateNewsletter:success';
return {
action: 'updateNewsletter:success',
member: member
action,
member: member,
popupNotification: createPopupNotification({
type: action,
autoHide: true,
closeable: true,
state
})
};
}
}
async function updateProfile({data, api}) {
async function updateProfile({data, state, api}) {
const {name, subscribed} = data;
const member = await api.member.update({name, subscribed});
if (!member) {
const action = 'updateProfile:failed';
return {
action: 'updateProfile:failed'
action,
popupNotification: createPopupNotification({
type: action,
autoHide: true,
closeable: true,
status: 'error',
state
})
};
} else {
const action = 'updateProfile:success';
return {
action: 'updateProfile:success',
action,
member: member,
page: 'accountHome'
page: 'accountHome',
popupNotification: createPopupNotification({
type: action,
autoHide: true,
closeable: true,
status: 'success',
state
})
};
}
}
@ -179,18 +257,18 @@ const Actions = {
updateEmail,
updateSubscription,
cancelSubscription,
updateMember,
updateNewsletter,
updateProfile,
clearPopupNotification,
editBilling,
checkoutPlan
};
/** Handle actions in the App, returns updated state */
export default async function ActionHandler({action, data, updateState, state, api}) {
export default async function ActionHandler({action, data, state, api}) {
const handler = Actions[action];
if (handler) {
return await handler({data, updateState, state, api}) || {};
return await handler({data, state, api}) || {};
}
return {};
}

View file

@ -80,9 +80,23 @@ class NotificationContent extends React.Component {
this.props.onHideNotification();
}
componentDidUpdate() {
const {showPopup} = this.context;
if (!this.state.className && showPopup) {
this.setState({
className: 'slideout'
});
}
}
componentDidMount() {
const {autoHide, duration = 2000} = this.props;
if (autoHide) {
const {showPopup} = this.context;
if (showPopup) {
this.setState({
className: 'slideout'
});
} else if (autoHide) {
setTimeout(() => {
this.setState({
className: 'slideout'

View file

@ -72,6 +72,16 @@ class PopupContent extends React.Component {
);
}
renderPopupNotification() {
const {popupNotification} = this.context;
if (!popupNotification || !popupNotification.type) {
return null;
}
return (
<PopupNotification />
);
}
render() {
const {page, site} = this.context;
const {portal_plans: portalPlans} = site;
@ -90,7 +100,7 @@ class PopupContent extends React.Component {
return (
<div className='gh-portal-popup-wrapper'>
<div className={(hasMode(['preview', 'dev']) ? 'gh-portal-popup-container preview' : 'gh-portal-popup-container') + ' ' + popupWidthStyle} style={pageStyle} ref={node => (this.node = node)} tabIndex="-1">
{/* <PopupNotification /> */}
{this.renderPopupNotification()}
{this.renderActivePage()}
</div>
</div>

View file

@ -46,7 +46,7 @@ export const PopupNotificationStyles = `
transition: all 0.2s ease-in-out forwards;
opacity: 0.8;
}
.gh-portal-popupnotification .closeicon:hover {
opacity: 1.0;
}
@ -72,13 +72,99 @@ export const PopupNotificationStyles = `
}
`;
const CloseButton = ({hide = false}) => {
if (hide) {
return null;
}
return (
<CloseIcon className='closeicon' alt='Close' />
);
};
const NotificationText = ({type, status}) => {
if (type === 'updateNewsletter:success') {
return (
<p> Newsletter updated! </p>
);
} else if (type === 'updateSubscription:success') {
return (
<p> Subscription updated! </p>
);
} else if (type === 'updateProfile:success') {
return (
<p> Profile Updated! </p>
);
} else if (type === 'updateProfile:failed') {
return (
<p> Failed to update profile! </p>
);
}
const label = status === 'success' ? 'Success' : 'Failed';
return (
<p> ${label}</p>
);
};
export default class PopupNotification extends React.Component {
static contextType = AppContext;
constructor() {
super();
this.state = {
className: '',
notificationType: ''
};
}
onAnimationEnd(e) {
if (e.animationName === 'popupnotification-slideout') {
this.context.onAction('clearPopupNotification');
}
}
componentDidUpdate() {
const {popupNotification} = this.context;
if (popupNotification.count !== this.state.count) {
clearTimeout(this.timeoutId);
this.handlePopupNotification({popupNotification});
}
}
handlePopupNotification({popupNotification}) {
if (popupNotification.autoHide) {
const {duration = 2000} = popupNotification;
this.timeoutId = setTimeout(() => {
this.setState({
className: 'slideout',
notificationCount: popupNotification.count
});
}, duration);
} else {
this.setState({
notificationCount: popupNotification.count
});
}
}
componentDidMount() {
const {popupNotification} = this.context;
this.handlePopupNotification({popupNotification});
}
componentWillUnmount() {
clearTimeout(this.timeoutId);
}
render() {
const {popupNotification} = this.context;
const {className} = this.state;
const {type, status, closeable} = popupNotification;
const statusClass = status ? ` ${status}` : '';
const slideClass = className ? ` ${className}` : '';
return (
<div className='gh-portal-popupnotification success'>
<p>Plan changed successfully</p>
<CloseIcon className='closeicon' alt='Close' />
<div className={`gh-portal-popupnotification${statusClass}${slideClass}`} onAnimationEnd={e => this.onAnimationEnd(e)}>
<NotificationText type={type} status={status} />
<CloseButton hide={!closeable} />
</div>
);
}

View file

@ -196,9 +196,6 @@ const AccountActions = ({member, site, action, openEditProfile, openUpdatePlan,
const {name, email, subscribed} = member;
let label = subscribed ? 'Subscribed to email newsletters' : 'Not subscribed to email newsletters';
if (action === 'updateNewsletter:success') {
label = '✓ Newsletter updated';
}
return (
<div className='gh-portal-list'>
<section>

View file

@ -186,10 +186,6 @@ export default class AccountProfilePage extends React.Component {
);
}
onToggleSubscription(e, subscribed) {
this.context.onAction('updateMember', {subscribed: !subscribed});
}
render() {
const {member} = this.context;
if (!member) {

View file

@ -104,7 +104,7 @@ function setupGhostApi({siteUrl = window.location.origin}) {
if (res.ok) {
return 'Success';
} else {
return 'Failed to send magic link';
throw new Error('Failed to send magic link email');
}
});
},