Skip to content

Commit

Permalink
Merge pull request #21671 from Yoast/show-notification-when-alert-fai…
Browse files Browse the repository at this point in the history
…ls-to-change-visibility

Show notification when alert fails to change visibility
  • Loading branch information
vraja-pro authored Oct 14, 2024
2 parents e76b959 + dae2b09 commit 4325a01
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 87 deletions.
134 changes: 80 additions & 54 deletions packages/js/src/dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import { Transition } from "@headlessui/react";
import { AdjustmentsIcon, BellIcon } from "@heroicons/react/outline";
import { __ } from "@wordpress/i18n";
import { useEffect, useMemo } from "@wordpress/element";
import { select } from "@wordpress/data";
import { useCallback, useEffect, useMemo } from "@wordpress/element";
import { select, useDispatch } from "@wordpress/data";
import { addQueryArgs } from "@wordpress/url";
import { SidebarNavigation, useSvgAria } from "@yoast/ui-library";
import { Notifications, SidebarNavigation, useSvgAria } from "@yoast/ui-library";
import PropTypes from "prop-types";
import { Link, useLocation, Outlet } from "react-router-dom";
import { MenuItemLink, YoastLogo } from "../shared-admin/components";
Expand Down Expand Up @@ -74,64 +74,90 @@ const App = () => {
}, [ notices ] );

const { pathname } = useLocation();
const alertToggleError = useSelectDashboard( "selectAlertToggleError", [], [] );
const { setAlertToggleError } = useDispatch( STORE_NAME );

const handleDismiss = useCallback( () => {
setAlertToggleError( null );
}, [ setAlertToggleError ] );

const linkParams = select( STORE_NAME ).selectLinkParams();
const webinarIntroSettingsUrl = addQueryArgs( "https://yoa.st/webinar-intro-settings", linkParams );

return (
<SidebarNavigation activePath={ pathname }>
<SidebarNavigation.Mobile
openButtonId="button-open-dashboard-navigation-mobile"
closeButtonId="button-close-dashboard-navigation-mobile"
/* translators: Hidden accessibility text. */
openButtonScreenReaderText={ __( "Open dashboard navigation", "wordpress-seo" ) }
/* translators: Hidden accessibility text. */
closeButtonScreenReaderText={ __( "Close dashboard navigation", "wordpress-seo" ) }
aria-label={ __( "Dashboard navigation", "wordpress-seo" ) }
>
<Menu idSuffix="-mobile" />
</SidebarNavigation.Mobile>
<div className="yst-p-4 min-[783px]:yst-p-8 yst-flex yst-gap-4">
<aside className="yst-sidebar yst-sidebar-nav yst-shrink-0 yst-hidden min-[783px]:yst-block yst-pb-6 yst-bottom-0 yst-w-56">
<SidebarNavigation.Sidebar>
<Menu />
</SidebarNavigation.Sidebar>
</aside>
<div className="yst-grow">
<div>
{ shouldShowWebinarPromotionNotificationInDashboard( STORE_NAME ) &&
<WebinarPromoNotification store={ STORE_NAME } url={ webinarIntroSettingsUrl } image={ null } />
}
{ notices.length > 0 && <div className="yst-space-y-3 yoast-new-dashboard-notices"> {
notices.map( ( notice, index ) => (
<Notice
key={ index }
id={ notice.id || "yoast-dashboard-notice-" + index }
title={ notice.header }
isDismissable={ notice.isDismissable }
<>
<SidebarNavigation activePath={ pathname }>
<SidebarNavigation.Mobile
openButtonId="button-open-dashboard-navigation-mobile"
closeButtonId="button-close-dashboard-navigation-mobile"
/* translators: Hidden accessibility text. */
openButtonScreenReaderText={ __( "Open dashboard navigation", "wordpress-seo" ) }
/* translators: Hidden accessibility text. */
closeButtonScreenReaderText={ __( "Close dashboard navigation", "wordpress-seo" ) }
aria-label={ __( "Dashboard navigation", "wordpress-seo" ) }
>
<Menu idSuffix="-mobile" />
</SidebarNavigation.Mobile>
<div className="yst-p-4 min-[783px]:yst-p-8 yst-flex yst-gap-4">
<aside className="yst-sidebar yst-sidebar-nav yst-shrink-0 yst-hidden min-[783px]:yst-block yst-pb-6 yst-bottom-0 yst-w-56">
<SidebarNavigation.Sidebar>
<Menu />
</SidebarNavigation.Sidebar>
</aside>
<div className="yst-grow">
<div>
{ shouldShowWebinarPromotionNotificationInDashboard( STORE_NAME ) &&
<WebinarPromoNotification store={ STORE_NAME } url={ webinarIntroSettingsUrl } image={ null } />
}
{ notices.length > 0 && <div className="yst-space-y-3 yoast-new-dashboard-notices"> {
notices.map( ( notice, index ) => (
<Notice
key={ index }
id={ notice.id || "yoast-dashboard-notice-" + index }
title={ notice.header }
isDismissable={ notice.isDismissable }
>
{ notice.content }
</Notice>
) )
}
</div> }
</div>
<div className="yst-space-y-6 yst-mb-8 xl:yst-mb-0">
<main>
<Transition
key={ pathname }
appear={ true }
show={ true }
enter="yst-transition-opacity yst-delay-100 yst-duration-300"
enterFrom="yst-opacity-0"
enterTo="yst-opacity-100"
>
{ notice.content }
</Notice>
) )
}
</div> }
</div>
<div className="yst-space-y-6 yst-mb-8 xl:yst-mb-0">
<main>
<Transition
key={ pathname }
appear={ true }
show={ true }
enter="yst-transition-opacity yst-delay-100 yst-duration-300"
enterFrom="yst-opacity-0"
enterTo="yst-opacity-100"
>
<Outlet />
</Transition>
</main>
<Outlet />
</Transition>
</main>
</div>
</div>
</div>
</div>
</SidebarNavigation>
</SidebarNavigation>
<Notifications
className="yst-mx-[calc(50%-50vw)] yst-transition-all lg:yst-left-44"
position="bottom-left"
>
{ alertToggleError && <Notifications.Notification
id="toggle-alert-error"
title={ __( "Something went wrong", "wordpress-seo" ) }
variant="error"
dismissScreenReaderLabel={ __( "Dismiss", "wordpress-seo" ) }
size="large"
autoDismiss={ 4000 }
onDismiss={ handleDismiss }
>
{ alertToggleError.type === "error" ? __( "This problem can't be hidden at this time. Please try again later.", "wordpress-seo" ) : __( "This notification can't be hidden at this time. Please try again later.", "wordpress-seo" ) }
</Notifications.Notification>
}
</Notifications>
</>
);
};

Expand Down
44 changes: 22 additions & 22 deletions packages/js/src/dashboard/components/alerts-list.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useContext, useCallback } from "@wordpress/element";
import { Fragment, useContext, useCallback } from "@wordpress/element";
import { useDispatch } from "@wordpress/data";
import PropTypes from "prop-types";
import { Button } from "@yoast/ui-library";
import { EyeOffIcon, EyeIcon } from "@heroicons/react/outline";
import classNames from "classnames";
import { AlertsContext } from "../contexts/alerts-context";


/**
* The alert item object.
*
Expand All @@ -22,30 +21,31 @@ const AlertItem = ( { id, nonce, dismissed, message } ) => {
const { toggleAlertStatus } = useDispatch( "@yoast/dashboard" );
const Eye = dismissed ? EyeIcon : EyeOffIcon;

const toggleAlert = useCallback( () => {
const toggleAlert = useCallback( async() => {
toggleAlertStatus( id, nonce, dismissed );
}, [ id, nonce, dismissed, toggleAlertStatus ] );

return <li key={ id } className="yst-border-b yst-border-slate-200 last:yst-border-b-0 yst-py-6 first:yst-pt-0 last:yst-pb-0">
<div className="yst-flex yst-justify-between yst-gap-5 yst-items-start">
<div className={ classNames( "yst-mt-1", dismissed && "yst-opacity-50" ) }>
<svg width="11" height="11" className={ bulletClass }>
<circle cx="5.5" cy="5.5" r="5.5" />
</svg>
</div>
<div
className={ classNames(
"yst-text-sm yst-text-slate-600 yst-grow",
dismissed && "yst-opacity-50" ) }
dangerouslySetInnerHTML={ { __html: message } }
/>

return <Fragment>
<li key={ id } className="yst-border-b yst-border-slate-200 last:yst-border-b-0 yst-py-6 first:yst-pt-0 last:yst-pb-0">
<div className="yst-flex yst-justify-between yst-gap-5 yst-items-start">
<div className={ classNames( "yst-mt-1", dismissed && "yst-opacity-50" ) }>
<svg width="11" height="11" className={ bulletClass }>
<circle cx="5.5" cy="5.5" r="5.5" />
</svg>
</div>
<div
className={ classNames(
"yst-text-sm yst-text-slate-600 yst-grow",
dismissed && "yst-opacity-50" ) }
dangerouslySetInnerHTML={ { __html: message } }
/>

<Button variant="secondary" size="small" className="yst-self-center yst-h-8" onClick={ toggleAlert }>
<Eye className="yst-w-4 yst-h-4 yst-text-neutral-700" />
</Button>
</div>
</li>;
<Button variant="secondary" size="small" className="yst-self-center yst-h-8" onClick={ toggleAlert }>
<Eye className="yst-w-4 yst-h-4 yst-text-neutral-700" />
</Button>
</div>
</li>
</Fragment>;
};

AlertItem.propTypes = {
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/dashboard/components/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Notifications = () => {
return (
<Paper>
<Paper.Content className="yst-flex yst-flex-col yst-gap-y-6">
<AlertsContext.Provider value={ notificationsTheme }>
<AlertsContext.Provider value={ { ...notificationsTheme } }>
<AlertsTitle counts={ notificationsAlertsList.length } title={ __( "Notifications", "wordpress-seo" ) }>
{ notificationsAlertsList.length === 0 && <p className="yst-mt-2 yst-text-sm">{ __( "No new notifications.", "wordpress-seo" ) }</p> }
</AlertsTitle>
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/dashboard/components/problems.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const Problems = () => {
return (
<Paper>
<Paper.Content className="yst-flex yst-flex-col yst-gap-y-6">
<AlertsContext.Provider value={ problemsTheme }>
<AlertsContext.Provider value={ { ...problemsTheme } }>
<AlertsTitle title={ __( "Problems", "wordpress-seo" ) } counts={ problemsList.length }>
<p className="yst-mt-2 yst-text-sm">{ __( "We have detected the following issues that affect the SEO of your site.", "wordpress-seo" ) }</p>
</AlertsTitle>
Expand Down
1 change: 0 additions & 1 deletion packages/js/src/dashboard/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@
* Keep constants centralized to avoid circular dependency problems.
*/
export const STORE_NAME = "@yoast/dashboard";

50 changes: 42 additions & 8 deletions packages/js/src/dashboard/store/alert-center.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createSlice, createSelector } from "@reduxjs/toolkit";
import { get } from "lodash";
import { ASYNC_ACTION_NAMES, ASYNC_ACTION_STATUS } from "../../shared-admin/constants";
import { ASYNC_ACTION_NAMES } from "../../shared-admin/constants";
import { select } from "@wordpress/data";
import { STORE_NAME } from "../constants";
import { get } from "lodash";

export const ALERT_CENTER_NAME = "alertCenter";

Expand All @@ -26,7 +26,7 @@ export function* toggleAlertStatus( id, nonce, hidden = false ) {
};
return { type: `${ TOGGLE_ALERT_VISIBILITY }/${ ASYNC_ACTION_NAMES.success }`, payload: { id } };
} catch ( error ) {
return { type: `${ TOGGLE_ALERT_VISIBILITY }/${ ASYNC_ACTION_NAMES.error }`, payload: error };
return { type: `${ TOGGLE_ALERT_VISIBILITY }/${ ASYNC_ACTION_NAMES.error }`, payload: { id } };
}
}

Expand All @@ -45,16 +45,37 @@ const toggleAlert = ( state, id ) => {
}
};

/**
* Sets an error in case of unsuccessful toggling..
*
* @param {object} state The state.
* @param {string} id The id of the alert that generated the error..
*
* @returns {void}
*/
const setAlertToggleError = ( state, id ) => {
const index = state.alerts.findIndex( ( alert ) => alert.id === id );
if ( index === -1 ) {
state.alertToggleError = null;
} else {
state.alertToggleError = state.alerts[ index ];
}
};

const slice = createSlice( {
name: ALERT_CENTER_NAME,
initialState: { alerts: [] },
initialState: { alertToggleError: null, alerts: [] },
reducers: {
toggleAlert,
setAlertToggleError,
},
extraReducers: ( builder ) => {
builder.addCase( `${ TOGGLE_ALERT_VISIBILITY }/${ ASYNC_ACTION_STATUS.success }`, ( state, { payload: { id } } ) => {
builder.addCase( `${ TOGGLE_ALERT_VISIBILITY }/${ ASYNC_ACTION_NAMES.success }`, ( state, { payload: { id } } ) => {
slice.caseReducers.toggleAlert( state, id );
} );
builder.addCase( `${ TOGGLE_ALERT_VISIBILITY }/${ ASYNC_ACTION_NAMES.error }`, ( state, { payload: { id } } ) => {
slice.caseReducers.setAlertToggleError( state, id );
} );
},
} );

Expand All @@ -69,7 +90,15 @@ export const getInitialAlertCenterState = slice.getInitialState;
* @param {object} state The state.
* @returns {array} The alerts.
*/
const selectAlerts = ( state ) => get( state, "alertCenter.alerts", [] );
const selectAlerts = ( state ) => get( state, `${ALERT_CENTER_NAME}.alerts`, [] );

/**
* Selector to get the alert toggle error.
*
* @param {object} state The state.
* @returns {string} id The id of the alert which caused the error..
*/
const selectAlertToggleError = ( state ) => get( state, `${ALERT_CENTER_NAME}.alertToggleError`, null );

export const alertCenterSelectors = {
selectActiveProblems: createSelector(
Expand All @@ -88,6 +117,7 @@ export const alertCenterSelectors = {
[ selectAlerts ],
( alerts ) => alerts.filter( ( alert ) => alert.type === "warning" && alert.dismissed )
),
selectAlertToggleError,
};

export const alertCenterActions = {
Expand All @@ -96,21 +126,25 @@ export const alertCenterActions = {
};

export const alertCenterControls = {
[ TOGGLE_ALERT_VISIBILITY ]: ( { payload } ) => {
[ TOGGLE_ALERT_VISIBILITY ]: async( { payload } ) => {
const formData = new URLSearchParams();
formData.append( "action", payload.hidden ? "yoast_restore_notification" : "yoast_dismiss_notification" );
formData.append( "notification", payload.id );
formData.append( "nonce", payload.nonce );

const ajaxUrl = select( STORE_NAME ).selectPreference( "ajaxUrl" );

fetch( ajaxUrl, {
const response = await fetch( ajaxUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: formData.toString(),
} );

if ( ! response.ok ) {
throw new Error( "Failed to dismiss notification" );
}
},
};

Expand Down

0 comments on commit 4325a01

Please sign in to comment.