From ca578b9bf1e9c37c93fe795ff0eecd61457d7591 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Wed, 5 Apr 2023 23:53:04 +0200 Subject: [PATCH 01/52] dashboard notices fix --- src/scripts/components/Notice.js | 18 ++++++++---------- src/scripts/components/NoticeAction.js | 21 ++++++++------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/scripts/components/Notice.js b/src/scripts/components/Notice.js index 00c84cbd..184cc824 100644 --- a/src/scripts/components/Notice.js +++ b/src/scripts/components/Notice.js @@ -39,8 +39,7 @@ export const Notice = ( props ) => { date = __( 'Just now' ), message, severity, - dismissible, - unread, + action, } = props; /** @@ -59,24 +58,23 @@ export const Notice = ( props ) => { className={ classnames( 'wp-notification', 'wp-notice-' + id, - dismissible, + action?.dismissible ? 'dismissible' : null, severity ? severity : null, - unread ? 'unread' : null, + action?.unread ? 'unread' : null, status ) } >

{ title }

- { message ?? ( -

- ) } + { message ? ( +

+ ) : null } - +
diff --git a/src/scripts/components/NoticeAction.js b/src/scripts/components/NoticeAction.js index f90612c5..d819308f 100644 --- a/src/scripts/components/NoticeAction.js +++ b/src/scripts/components/NoticeAction.js @@ -5,23 +5,18 @@ import { defaultContext } from '../store/constants'; /** * Renders an image or icon based on the type of notification * - * @param {Object} param - * @param {Object} param.action - * @param {string} param.context - * @param {string} param.onDismiss - callback to be called when the notice is dismissed - * @param {boolean} param.dismissible - whether the notice is dismissible or not + * @param {Object} param + * @param {Object} param.action + * @param {string} param.context + * @param {Function} param.onDismiss - callback to be called when the notice is dismissed * @return {JSX.Element} NoticeImage - the image or the icon wrapped into a div */ -const NoticeActions = ( { - action, - context, - dismissible = false, - onDismiss, -} ) => { +const NoticeActions = ( { action, context, onDismiss } ) => { const { acceptLink = '#', acceptMessage = __( 'Accept' ), dismissLabel = __( 'Dismiss' ), + dismissible = false, } = action; if ( context === defaultContext ) { @@ -46,7 +41,7 @@ const NoticeActions = ( { > { acceptMessage } - { dismissible && ( + { dismissible ? ( - ) } + ) : null } ); }; From 898cdda07b2f10c9ef1a208518f5a07c075d6b4f Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Thu, 6 Apr 2023 11:18:58 +0200 Subject: [PATCH 02/52] fixes drawer shadow position - #182 --- src/styles/notify/hub/layout.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/notify/hub/layout.scss b/src/styles/notify/hub/layout.scss index 20796a6b..e1b7083b 100644 --- a/src/styles/notify/hub/layout.scss +++ b/src/styles/notify/hub/layout.scss @@ -22,6 +22,7 @@ #wp-admin-bar-wp-notify.active & { opacity: 1; transform: translateX(-100%); + padding-bottom: 0; // moves the shadow at the end of sidebar } // reset child icons color since by default the icon is lightened when the mouse is in hover state From 44adecc158b10facf7ee37fd2572012a25c5aa21 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Thu, 6 Apr 2023 11:26:12 +0200 Subject: [PATCH 03/52] overlays with the bell icon the drawer when is active - ref #191 --- .editorconfig | 2 +- src/styles/notify/hub/admin-bar.scss | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1d658f5d..2cbaaa17 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,7 @@ insert_final_newline = true trim_trailing_whitespace = true indent_style = tab -[*.{js}] +[*.{js,scss}] indent_style = space indent_size = 2 diff --git a/src/styles/notify/hub/admin-bar.scss b/src/styles/notify/hub/admin-bar.scss index 92588058..b0cb0475 100644 --- a/src/styles/notify/hub/admin-bar.scss +++ b/src/styles/notify/hub/admin-bar.scss @@ -12,7 +12,14 @@ top: 3px; } - /* the label is the red dot over the bell */ + /* When the drawer is enabled transforms the bell icon to overlay the drawer */ + &.active > .ab-item .ab-icon { + filter: invert(1); + position: relative; + z-index: 100002; + } + + /* The label is the red dot over the bell */ .ab-label { position: absolute; width: 6px; From fa8e67bdfc24652080cbff0081054e150103e0ee Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Thu, 6 Apr 2023 11:44:04 +0200 Subject: [PATCH 04/52] fix wrong date for new notifications --- src/scripts/components/Notice.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/scripts/components/Notice.js b/src/scripts/components/Notice.js index 184cc824..5a854847 100644 --- a/src/scripts/components/Notice.js +++ b/src/scripts/components/Notice.js @@ -12,7 +12,6 @@ import { NoticeActions } from './NoticeAction'; import classnames from 'classnames'; import { purify } from '../utils/sanitization'; import moment from 'moment'; -import { __ } from '@wordpress/i18n'; import { defaultContext, NOTIFY_NAMESPACE } from '../store/constants'; import { dispatch } from '@wordpress/data'; @@ -36,7 +35,7 @@ export const Notice = ( props ) => { status, context = defaultContext, source = 'WordPress', - date = __( 'Just now' ), + date = Date.now() * 0.001, message, severity, action, From d92fba9c9e74d31c6280c3a05d3578873d63b52c Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Thu, 6 Apr 2023 17:39:52 +0200 Subject: [PATCH 05/52] drawer wip --- .../{utils/drawer.js => components/Drawer.js} | 84 +++++++++++-------- src/scripts/components/NoticesArea.js | 2 +- src/scripts/utils/effects.js | 25 ++++++ src/scripts/wp-notify.js | 6 ++ 4 files changed, 80 insertions(+), 37 deletions(-) rename src/scripts/{utils/drawer.js => components/Drawer.js} (58%) diff --git a/src/scripts/utils/drawer.js b/src/scripts/components/Drawer.js similarity index 58% rename from src/scripts/utils/drawer.js rename to src/scripts/components/Drawer.js index b5bf54ea..95f1a113 100644 --- a/src/scripts/utils/drawer.js +++ b/src/scripts/components/Drawer.js @@ -1,6 +1,6 @@ import { dispatch } from '@wordpress/data'; -import { WEEK_IN_SECONDS } from '../components/NoticesArea'; import { NOTIFY_NAMESPACE } from '../store/constants'; +import { __ } from '@wordpress/i18n'; /** * It clears the notices in the selected context @@ -11,42 +11,28 @@ export const clearNotifyDrawer = ( context ) => { dispatch( NOTIFY_NAMESPACE ).clear( context ); }; -/** - * At the moment the function return the notifications if the split by isn't set to "date" - * - * @param {Array} notifications - * @param {string} by - * - * @return {Array} two list of Notifications, one for the new and one for the old - */ -export const getSorted = ( notifications, by = 'date' ) => { - const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; - if ( Limit ) { - return notifications.reduce( - ( [ current, past ], item ) => { - return item.date >= Limit - ? [ [ ...current, item ], past ] - : [ current, [ ...past, item ] ]; - }, - [ [], [] ] - ); +export const wpNotifyHub = document.getElementById( 'wp-admin-bar-wp-notify' ); + +export const toggleNotifyDrawer = ( e ) => { + if ( wpNotifyHub.isActive && ! wpNotifyHub.contains( e.target ) ) { + e.stopPropagation(); + enableNotifyDrawer( e ); + } else { + e.stopPropagation(); + disableNotifyDrawer( e ); } - return notifications; }; -export const wpNotifyHub = document.getElementById( 'wp-admin-bar-wp-notify' ); - /** * When the user clicks on the notification drawer, the drawer is disabled * - * @param {Event} e * @callback {disableNotifyDrawer} disableNotifyDrawer */ -export const disableNotifyDrawer = ( e ) => { - e.stopPropagation(); +export const disableNotifyDrawer = () => { + wpNotifyHub.isActive = false; wpNotifyHub.classList.remove( 'active' ); - document.onkeydown = null; - document.body.removeEventListener( 'click', disableNotifyDrawer ); + // remove document body the listener to disable the drawer + drawerExitKey( false ); }; /** @@ -59,21 +45,32 @@ export const disableNotifyDrawer = ( e ) => { */ export const enableNotifyDrawer = ( e ) => { e.stopPropagation(); - if ( ! wpNotifyHub.classList.contains( 'active' ) ) { - wpNotifyHub.classList.add( 'active' ); - document.body.addEventListener( 'click', disableNotifyDrawer ); - document.onkeydown = ( ev ) => { + wpNotifyHub.isActive = true; + wpNotifyHub.classList.add( 'active' ); + // listen for clicks outside the drawer to disable it + drawerExitKey( true ); +}; + +function drawerExitKey( enabled ) { + if ( enabled ) { + document.body.onkeydown = ( ev ) => { if ( 'key' in ev && ( ev.key === 'Escape' || ev.key === 'Esc' ) ) { - disableNotifyDrawer(); + disableNotifyDrawer( ev ); } }; + } else { + document.body.onclick = null; + document.body.onkeydown = null; } -}; +} /** * Action handler for the notification drawer */ if ( wpNotifyHub ) { + const wpNotifyHubIcon = wpNotifyHub.querySelector( '.ab-item' ); + + wpNotifyHub.isActive = false; /** * Notification hub * Handle click on wp-admin bar bell icon that show the WP-Notify sidebar @@ -82,7 +79,22 @@ if ( wpNotifyHub ) { * @event enableNotifyDrawer - When the user clicks or focus on the notification drawer, the drawer is enabled * @event disableNotifyDrawer - on focus out */ - wpNotifyHub.addEventListener( 'click', enableNotifyDrawer ); - wpNotifyHub.addEventListener( 'focus', enableNotifyDrawer, true ); + wpNotifyHubIcon.onclick = toggleNotifyDrawer; + + // keyboard event when the user focus on the notification drawer + wpNotifyHub.addEventListener( 'focus', toggleNotifyDrawer, true ); wpNotifyHub.addEventListener( 'blur', disableNotifyDrawer, true ); } + +export default () => { + return ( + <> +
+

+ { __( 'Notifications' ) } +

+
+
+ + ); +}; diff --git a/src/scripts/components/NoticesArea.js b/src/scripts/components/NoticesArea.js index 91a537e2..2d858b73 100644 --- a/src/scripts/components/NoticesArea.js +++ b/src/scripts/components/NoticesArea.js @@ -5,7 +5,7 @@ import { defaultContext, NOTIFY_NAMESPACE } from '../store/constants'; import { NoticeEmpty } from './NoticeEmpty'; import { NoticeHubSectionHeader } from './NoticeHubSectionHeader'; import { NoticesLoop } from './NoticesLoop'; -import { getSorted } from '../utils/drawer'; +import { getSorted } from '../utils/effects'; import { NoticeHubFooter } from './NoticeHubFooter'; export const WEEK_IN_SECONDS = 1000 - 3600 * 24 * 7; diff --git a/src/scripts/utils/effects.js b/src/scripts/utils/effects.js index 12cd5250..74fdf320 100644 --- a/src/scripts/utils/effects.js +++ b/src/scripts/utils/effects.js @@ -1,3 +1,5 @@ +import { WEEK_IN_SECONDS } from '../components/NoticesArea'; + /** * Delay returns a promise that resolves after the specified number of milliseconds. * @@ -6,3 +8,26 @@ * @return {Promise} - the resolution of the promise */ export const delay = ( ms ) => new Promise( ( f ) => setTimeout( f, ms ) ); + +/** + * At the moment the function return the notifications if the split by isn't set to "date" + * + * @param {Array} notifications + * @param {string} by + * + * @return {Array} two list of Notifications, one for the new and one for the old + */ +export const getSorted = ( notifications, by = 'date' ) => { + const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; + if ( Limit ) { + return notifications.reduce( + ( [ current, past ], item ) => { + return item.date >= Limit + ? [ [ ...current, item ], past ] + : [ current, [ ...past, item ] ]; + }, + [ [], [] ] + ); + } + return notifications; +}; diff --git a/src/scripts/wp-notify.js b/src/scripts/wp-notify.js index cd43b3d1..c8b6e72c 100644 --- a/src/scripts/wp-notify.js +++ b/src/scripts/wp-notify.js @@ -4,10 +4,16 @@ import { dispatch, select } from '@wordpress/data'; /** WP Notify - Components */ import { NoticesArea } from './components/NoticesArea'; +import Drawer from './components/Drawer'; /** The store default data */ import { NOTIFY_NAMESPACE, contexts } from './store/constants'; +render( + createElement( Drawer ), + document.getElementById( `wp-notification-hub` ) +); + /** * The redux store */ From b443e62a7da9bdbd111fc9e5e7ac02ae56ff0e67 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Fri, 7 Apr 2023 14:25:46 +0200 Subject: [PATCH 06/52] wip - drawer as a React component --- src/scripts/components/Drawer.js | 134 ++++++------------ .../components/NoticeHubSectionHeader.js | 2 +- src/scripts/utils/effects.js | 11 ++ src/scripts/wp-notify.js | 18 ++- 4 files changed, 69 insertions(+), 96 deletions(-) diff --git a/src/scripts/components/Drawer.js b/src/scripts/components/Drawer.js index 95f1a113..d4ebfa9d 100644 --- a/src/scripts/components/Drawer.js +++ b/src/scripts/components/Drawer.js @@ -1,100 +1,58 @@ -import { dispatch } from '@wordpress/data'; -import { NOTIFY_NAMESPACE } from '../store/constants'; import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; -/** - * It clears the notices in the selected context - * - * @param {string} context - The context of the notices. This is used to determine which notices to clear. - */ -export const clearNotifyDrawer = ( context ) => { - dispatch( NOTIFY_NAMESPACE ).clear( context ); -}; - -export const wpNotifyHub = document.getElementById( 'wp-admin-bar-wp-notify' ); - -export const toggleNotifyDrawer = ( e ) => { - if ( wpNotifyHub.isActive && ! wpNotifyHub.contains( e.target ) ) { - e.stopPropagation(); - enableNotifyDrawer( e ); - } else { - e.stopPropagation(); - disableNotifyDrawer( e ); - } -}; - -/** - * When the user clicks on the notification drawer, the drawer is disabled - * - * @callback {disableNotifyDrawer} disableNotifyDrawer - */ -export const disableNotifyDrawer = () => { - wpNotifyHub.isActive = false; - wpNotifyHub.classList.remove( 'active' ); - // remove document body the listener to disable the drawer - drawerExitKey( false ); +export const wpNotifyDrawer = () => { + return ( +
+

+ { __( 'Notifications' ) } +

+
+
+ ); }; -/** - * Enable the notification drawer - * If the notification drawer is not active, add the active class to the notification drawer and add an event listener to the body to disable the notification drawer - * The first thing we do is stop the propagation of the event. This is important because we don't want the event to bubble up to the body and trigger the disableNotifyDrawer function - * - * @callback {enableNotifyDrawer} enableNotifyDrawer - * @param {Event} e - The event object. - */ -export const enableNotifyDrawer = ( e ) => { - e.stopPropagation(); - wpNotifyHub.isActive = true; - wpNotifyHub.classList.add( 'active' ); - // listen for clicks outside the drawer to disable it - drawerExitKey( true ); +export const wpNotifyHubIcon = () => { + return ( +
+ + { __( 'Notifications' ) } +
+ ); }; -function drawerExitKey( enabled ) { - if ( enabled ) { - document.body.onkeydown = ( ev ) => { - if ( 'key' in ev && ( ev.key === 'Escape' || ev.key === 'Esc' ) ) { - disableNotifyDrawer( ev ); - } - }; - } else { - document.body.onclick = null; - document.body.onkeydown = null; - } -} - -/** - * Action handler for the notification drawer - */ -if ( wpNotifyHub ) { - const wpNotifyHubIcon = wpNotifyHub.querySelector( '.ab-item' ); - - wpNotifyHub.isActive = false; - /** - * Notification hub - * Handle click on wp-admin bar bell icon that show the WP-Notify sidebar - * - * @member {HTMLElement} wpNotifyHub - the Notification Hub Controller - * @event enableNotifyDrawer - When the user clicks or focus on the notification drawer, the drawer is enabled - * @event disableNotifyDrawer - on focus out - */ - wpNotifyHubIcon.onclick = toggleNotifyDrawer; - - // keyboard event when the user focus on the notification drawer - wpNotifyHub.addEventListener( 'focus', toggleNotifyDrawer, true ); - wpNotifyHub.addEventListener( 'blur', disableNotifyDrawer, true ); -} - export default () => { + const [ isActive, setIsActive ] = useState( false ); + + const toggleDrawer = () => { + setIsActive( ! isActive ); + }; + + const handleIconKeyDown = ( ev ) => { + if ( ( 'key' in ev && ev.key === 'Enter' ) || ev.key === ' ' ) { + // Activate item on "Enter" or "Space" key press + setIsActive( true ); + } + }; + + const handleDrawerKeyDown = ( ev ) => { + if ( 'key' in ev && ( ev.key === 'Escape' || ev.key === 'Esc' ) ) { + // Close the drawer on "Escape" or "Esc" key press + setIsActive( false ); + } + }; + return ( <> -
-

- { __( 'Notifications' ) } -

-
-
+ toggleDrawer( event ) } + onKeyDown={ ( event ) => handleIconKeyDown( event ) } + /> + ); }; diff --git a/src/scripts/components/NoticeHubSectionHeader.js b/src/scripts/components/NoticeHubSectionHeader.js index d57a200f..72267d5c 100644 --- a/src/scripts/components/NoticeHubSectionHeader.js +++ b/src/scripts/components/NoticeHubSectionHeader.js @@ -1,7 +1,7 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { check } from '@wordpress/icons'; -import { clearNotifyDrawer } from '../utils/drawer'; +import { clearNotifyDrawer } from '../utils/effects'; /** * The section header for the notices section drawer. diff --git a/src/scripts/utils/effects.js b/src/scripts/utils/effects.js index 74fdf320..566cadc9 100644 --- a/src/scripts/utils/effects.js +++ b/src/scripts/utils/effects.js @@ -1,4 +1,6 @@ import { WEEK_IN_SECONDS } from '../components/NoticesArea'; +import { dispatch } from '@wordpress/data'; +import { NOTIFY_NAMESPACE } from '../store/constants'; /** * Delay returns a promise that resolves after the specified number of milliseconds. @@ -31,3 +33,12 @@ export const getSorted = ( notifications, by = 'date' ) => { } return notifications; }; + +/** + * It clears the notices in the selected context + * + * @param {string} context - The context of the notices. This is used to determine which notices to clear. + */ +export const clearNotifyDrawer = ( context ) => { + dispatch( NOTIFY_NAMESPACE ).clear( context ); +}; diff --git a/src/scripts/wp-notify.js b/src/scripts/wp-notify.js index 72e4404a..a49fe189 100644 --- a/src/scripts/wp-notify.js +++ b/src/scripts/wp-notify.js @@ -9,10 +9,14 @@ import Drawer from './components/Drawer'; /** The store default data */ import { NOTIFY_NAMESPACE, contexts } from './store/constants'; -render( - createElement( Drawer ), - document.getElementById( `wp-notification-hub` ) -); +/** Get the Notification Hub area (admin bar) */ +const adminBarWpNotify = document.getElementById( 'wp-admin-bar-wp-notify' ); + +/** Creates a root for Notification Hub area */ +const hubRoot = createRoot( adminBarWpNotify ); + +/** Init the Notification Hub component */ +hubRoot.render( ); /** * The redux store @@ -94,17 +98,17 @@ select( NOTIFY_NAMESPACE ).fetchUpdates(); */ contexts.forEach( ( context ) => { /** Get the component container */ - const container = document.getElementById( `wp-notify-${ context }` ); + const notifyContainer = document.getElementById( `wp-notify-${ context }` ); /** Creates a root for NoticesArea component. */ - const root = createRoot( container ); + const notifyRoot = createRoot( notifyContainer ); /** * Renders the component into the specified context * * @member {HTMLElement} notifyDash - the area that will host the notifications */ - root.render( + notifyRoot.render( Date: Fri, 7 Apr 2023 16:52:01 +0200 Subject: [PATCH 07/52] Registers the drawer differently from the "notification areas" --- src/scripts/wp-notify.js | 41 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/scripts/wp-notify.js b/src/scripts/wp-notify.js index a49fe189..a2018985 100644 --- a/src/scripts/wp-notify.js +++ b/src/scripts/wp-notify.js @@ -9,15 +9,6 @@ import Drawer from './components/Drawer'; /** The store default data */ import { NOTIFY_NAMESPACE, contexts } from './store/constants'; -/** Get the Notification Hub area (admin bar) */ -const adminBarWpNotify = document.getElementById( 'wp-admin-bar-wp-notify' ); - -/** Creates a root for Notification Hub area */ -const hubRoot = createRoot( adminBarWpNotify ); - -/** Init the Notification Hub component */ -hubRoot.render( ); - /** * The redux store */ @@ -93,10 +84,7 @@ contexts.forEach( ( context ) => /** after registering contexts we could fetch the notifications */ select( NOTIFY_NAMESPACE ).fetchUpdates(); -/** - * Loops into contexts and adds a NoticesArea component for each one - */ -contexts.forEach( ( context ) => { +function addContext( context ) { /** Get the component container */ const notifyContainer = document.getElementById( `wp-notify-${ context }` ); @@ -108,13 +96,28 @@ contexts.forEach( ( context ) => { * * @member {HTMLElement} notifyDash - the area that will host the notifications */ - notifyRoot.render( - + notifyRoot.render( ); +} + +function addDrawer() { + /** Get the Notification Hub area (admin bar) */ + const adminBarWpNotify = document.getElementById( + 'wp-admin-bar-wp-notify' ); -} ); + + /** Creates a root for Notification Hub area */ + const hubRoot = createRoot( adminBarWpNotify ); + + /** Init the Notification Hub component */ + hubRoot.render( ); +} + +/** + * Loops into contexts and adds a NoticesArea component for each one + */ +contexts.forEach( ( context ) => + context === 'adminbar' ? addDrawer() : addContext( context ) +); /** * exports notify store functions for further uses From 98b38745a60ba24f4f2b134c4665a0e46ca8ee8e Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 09:51:13 -0700 Subject: [PATCH 08/52] add Notification type // johnhooks --- includes/restapi/fake_api.json | 49 ++++++++++++---------------------- src/scripts/store/index.js | 43 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/includes/restapi/fake_api.json b/includes/restapi/fake_api.json index 485f45e6..62fcefa3 100644 --- a/includes/restapi/fake_api.json +++ b/includes/restapi/fake_api.json @@ -5,14 +5,14 @@ "title": "Try this new Notification feature", "source": "#WP-Notify", "date": 1664992015, - "message": "We have just added a wonderful feature! You might want to give it a try so click on the bell icon on the right side of the adminbar.
", + "message": "👋 Hello from the WP Feature Notifications team! Thank you for testing out the plugin. You might want to give it a try so click on the bell icon on the right side of the adminbar.", + "dismissible": false, "icon": { "src": "https://raw.githubusercontent.com/erikyo/wp-notify/design_implementation/src/images/i.svg" }, "action": { - "acceptMessage": "Try this new feature", - "acceptLink": "https://github.com/WordPress/wp-notify", - "dismissible": false + "acceptMessage": "Check out the source", + "acceptLink": "https://github.com/WordPress/wp-feature-notifications" } }, { @@ -22,14 +22,14 @@ "date": 1654866071, "message": "This is an example of on-page message variant #1. It has a title, a message, an image, an action button with a URL, is dismissable.", "source": "#Test", + "dismissible": true, + "unread": true, "icon": { "src": "https://source.unsplash.com/random/400×400/?notify" }, "action": { "acceptMessage": "TEST", - "acceptLink": "https://github.com/WordPress/wp-notify", - "dismissible": true, - "unread": true + "acceptLink": "https://github.com/WordPress/wp-notify" } }, { @@ -48,26 +48,11 @@ "source": "#WP-Notify", "date": 1654866091, "message": "This is an example of on-page message variant #2. It has a title, a message, a custom date, an action button with a URL, is dismissable, but has no images.", + "dismissible": true, + "unread": true, "action": { "acceptMessage": "OK", - "acceptLink": "https://github.com/WordPress/wp-notify", - "dismissible": true, - "unread": true - } - }, - { - "id": 5, - "context": "dashboard", - "date": 1654866101, - "title": "Message variant #3", - "message": "if you're wondering where notice #2 is try looking at the adminbar at the top right near the bell icon 😉.", - "icon": { - "src": "https://gifimage.net/wp-content/uploads/2018/10/animation-notification-gif-2.gif" - }, - "action": { - "acceptLink": "https://github.com/WordPress/wp-notify", - "dismissible": true, - "unread": true + "acceptLink": "https://github.com/WordPress/wp-notify" } }, { @@ -76,10 +61,10 @@ "message": "WordPress was successfully updated to version 6.1", "date": 1664992015, "source": "WordPress", + "unread": true, "action": { "acceptMessage": "Read what's new in 6.1", - "acceptLink": "#", - "unread": true + "acceptLink": "#" } }, { @@ -106,24 +91,24 @@ "id": 9, "title": "WordPress", "message": "WordPress was successfully updated to version 6.1", + "source": "WordPress", + "date": 1654840000, "action": { "acceptMessage": "Read what's new in 6.1", - "acceptLink": "#", - "source": "WordPress", - "date": 1654840000 + "acceptLink": "#" } }, { "id": 10, "message": "There is a new version of Contact Form 7 available.", + "source": "Plugins Updates", "date": 1654830000, "icon": { "src": "https://ps.w.org/contact-form-7/assets/icon-256x256.png" }, "action": { "acceptMessage": "Update now", - "acceptLink": "#", - "source": "Plugins Updates" + "acceptLink": "#" } }, { diff --git a/src/scripts/store/index.js b/src/scripts/store/index.js index 9455eacb..8126c473 100644 --- a/src/scripts/store/index.js +++ b/src/scripts/store/index.js @@ -7,6 +7,49 @@ import * as selectors from './selectors'; import * as controls from './controls'; import * as resolvers from './resolvers'; +/** + * @typedef {Object} DashiconsIcon The Dashicons icon type. + * @property {string} dashicons The Dashicons slug of the icon. + */ + +/** + * @typedef {Object} ImageIcon The image icon type. + * @property {string} src The url of the image icon. + */ + +/** + * @typedef {Object} SvgIcon The SVG icon type. + * @property {string} svg The SVG markup of the icon. + */ + +/** + * @typedef {DashiconsIcon|ImageIcon|SvgIcon} NoticeIcon The notification icon type. + */ + +/** + * @typedef {Object} NoticeAction The notification action type. + * @property {string} acceptLink The url of the action. + * @property {string} acceptMessage The message content of the action. + */ + +/** + * @typedef {Object} Notification The notification type. + * @property {NoticeAction=} action The optional action associated to the notification. + * @property {string=} context The rendering context of the notification. + * @property {number} date The datetime from which the notification was emitted. + * @property {boolean} dismissible Predicate of whether or not the notification can be dismissed. + * @property {NoticeIcon=} icon The optional icon. + * @property {number} id The database id of the notification message. + * @property {string=} message The message content of the notification. + * @property {string=} source The source of the notification. + * @property {string} title The title of the notification message. + * @property {boolean} unread Predicate of whether or not the notification is in an unread state. + */ + +/** + * @typedef {Record} State The notifications redux store type. + */ + /** * Creating a store for the redux state. * From c58243452dcc2499155b64248c5cbaff402746dd Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 15:49:01 -0700 Subject: [PATCH 09/52] feature: type store --- jsconfig.json | 2 +- src/index.d.ts | 11 ++++ src/scripts/store/actions.js | 54 +++++++++++++++++--- src/scripts/store/constants.js | 5 +- src/scripts/store/controls.js | 7 ++- src/scripts/store/index.js | 48 +++++++++++++++--- src/scripts/store/reducer.js | 12 +++-- src/scripts/store/selectors.js | 23 ++++++--- src/scripts/store/utils.js | 5 +- src/scripts/utils/drawer.js | 92 ++++++++++++++++++++++++++++++++++ src/scripts/wp-notify.js | 9 ++-- 11 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 src/index.d.ts create mode 100644 src/scripts/utils/drawer.js diff --git a/jsconfig.json b/jsconfig.json index c8d11b3a..1e783f0a 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "checkJs": true, - "types": [ "jest" ], + "types": [ "./src/index.d.ts", "jest" ], "baseUrl": "./src" }, "include": [ "./src/**/*.js", "./src/**/*.jsx" ], diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 00000000..db8ccf00 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,11 @@ +import { CurriedSelectorsOf } from '@wordpress/data/build-types/types' +import { NoticeStore, NOTIFY_NAMESPACE } from './scripts/store' + +declare global { + interface Window { wp: { notify: any }; wp_notify_data?: { settingsPage: string } } +} + +declare module '@wordpress/data' { + function dispatch( key: NOTIFY_NAMESPACE ): typeof import( './scripts/store/actions' ); + function select( key: NOTIFY_NAMESPACE ): CurriedSelectorsOf< NoticeStore >; +} diff --git a/src/scripts/store/actions.js b/src/scripts/store/actions.js index 22a61613..bf93c8b9 100644 --- a/src/scripts/store/actions.js +++ b/src/scripts/store/actions.js @@ -1,41 +1,83 @@ +/* eslint-disable jsdoc/require-returns-type */ + +/** + * @typedef {import('./index').Notice} Notice + */ + +/** + * Action creator to hydrate a notice from the store. + * + * @param {Notice[]} payload The notices to hydrate. + * @return A redux action. + */ export const hydrate = ( payload ) => { return { - type: 'HYDRATE', + type: /** @type {'HYDRATE'} */ ( 'HYDRATE' ), payload, }; }; +/** + * Action creator to clear a notification context from the store. + * + * @param {string} context The slug of the context to clear. + * @return A redux action. + */ export const clear = ( context ) => { return { - type: 'CLEAR', + type: /** @type {'CLEAR'} */ ( 'CLEAR' ), context, }; }; +/** + * Action creator to add a notice to the store. + * + * @param {Notice} payload The notice to add. + * @return A redux action. + */ export const addNotice = ( payload ) => { return { - type: 'ADD', + type: /** @type {'ADD'} */ ( 'ADD' ), payload, }; }; +/** + * Action creator to remove a notice from the store. + * + * @param {number} id The id of the notice to remove. + * @return A redux action. + */ export const removeNotice = ( id ) => { return { - type: 'DELETE', + type: /** @type {'DELETE'} */ ( 'DELETE' ), id, }; }; +/** + * Action creator to update a notice in the store. + * + * @param {Notice} payload + * @return A redux action. + */ export const updateNotice = ( payload ) => { return { - type: 'UPDATE', + type: /** @type {'UPDATE'} */ ( 'UPDATE' ), payload, }; }; +/** + * Action creator to fetch notices. + * + * @param {string} path The REST API route from which to fetch notices. + * @return A redux action. + */ export const fetchAPI = ( path = '' ) => { return { - type: 'FETCH', + type: /** @type {'FETCH'} */ ( 'FETCH' ), path, }; }; diff --git a/src/scripts/store/constants.js b/src/scripts/store/constants.js index a8c85dba..82bc2331 100644 --- a/src/scripts/store/constants.js +++ b/src/scripts/store/constants.js @@ -1,4 +1,3 @@ -/* global wp_notify_data */ /** * @member {string} NOTIFY_NAMESPACE WP-Notify namespace */ @@ -24,4 +23,6 @@ export const contexts = [ defaultContext, 'dashboard' ]; */ export const settingsPageUrl = // eslint-disable-next-line camelcase - typeof wp_notify_data !== 'undefined' ? wp_notify_data?.settingsPage : ''; + typeof window.wp_notify_data !== 'undefined' + ? window.wp_notify_data?.settingsPage + : ''; diff --git a/src/scripts/store/controls.js b/src/scripts/store/controls.js index 7c7881e3..1ae131b6 100644 --- a/src/scripts/store/controls.js +++ b/src/scripts/store/controls.js @@ -1,11 +1,14 @@ import apiFetch from '@wordpress/api-fetch'; import { API_PATH } from './constants'; +/** + * @typedef {import('./index').Notice} Notice + */ /** * Fetches the wp-notify rest api endpoint for the specified endpoint * - * @param {string} action - the action to execute - * @return {Promise} - the Promise with the results + * @param {{path: string}} action The action to execute + * @return {Promise} The Promise with the results */ export const FETCH = ( action ) => { return apiFetch( { diff --git a/src/scripts/store/index.js b/src/scripts/store/index.js index 8126c473..3ebb7061 100644 --- a/src/scripts/store/index.js +++ b/src/scripts/store/index.js @@ -8,6 +8,26 @@ import * as controls from './controls'; import * as resolvers from './resolvers'; /** + <<<<<<< Updated upstream + ======= + * + * @typedef {import('@wordpress/data/build-types/types').ReduxStoreConfig} StoreConfig + * @typedef {import('@wordpress/data/build-types/types').StoreDescriptor} NoticeStore + */ + +/** + * @template {Record} T + * @template {keyof T} K + * @typedef {T[K]} ValuesOf + */ + +/** + * @typedef {ReturnType>} Action + */ + +/** + >>>>>>> Stashed changes + * * @typedef {Object} DashiconsIcon The Dashicons icon type. * @property {string} dashicons The Dashicons slug of the icon. */ @@ -33,7 +53,12 @@ import * as resolvers from './resolvers'; */ /** + <<<<<<< Updated upstream + * * @typedef {Object} Notification The notification type. + ======= + * @typedef {Object} Notice The notification type. + >>>>>>> Stashed changes * @property {NoticeAction=} action The optional action associated to the notification. * @property {string=} context The rendering context of the notification. * @property {number} date The datetime from which the notification was emitted. @@ -47,21 +72,28 @@ import * as resolvers from './resolvers'; */ /** + <<<<<<< Updated upstream + * * @typedef {Record} State The notifications redux store type. + ======= + * @typedef {Record} State The notifications redux store type. + >>>>>>> Stashed changes */ /** * Creating a store for the redux state. * - * @return {store} A Redux store that lets you read the state, dispatch actions and subscribe to changes. + * A Redux store that lets you read the state, dispatch actions and subscribe to changes. */ -const store = createReduxStore( NOTIFY_NAMESPACE, { - reducer, - actions, - selectors, - controls, - resolvers, -} ); +const store = /** @type {NoticeStore} */ ( + createReduxStore( NOTIFY_NAMESPACE, { + reducer, + actions, + selectors, + controls, + resolvers, + } ) +); register( store ); diff --git a/src/scripts/store/reducer.js b/src/scripts/store/reducer.js index 0c70579f..52a9f040 100644 --- a/src/scripts/store/reducer.js +++ b/src/scripts/store/reducer.js @@ -1,13 +1,16 @@ import { findContext } from './utils'; +/** + * @typedef {import('redux').Reducer} NoticeReducer + * @typedef {import('./index').State} State + * @typedef {import('./index').Action} Action + */ + /** * Reducer returning the next notices state. The notices state is an object * where each key is a context, its value an array of notice objects. * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. + * @type {NoticeReducer} */ const reducer = ( state = {}, action ) => { switch ( action.type ) { @@ -21,7 +24,6 @@ const reducer = ( state = {}, action ) => { }; } ); return updated; - case 'ADD': return { ...state, diff --git a/src/scripts/store/selectors.js b/src/scripts/store/selectors.js index 959c6e3f..455b6922 100644 --- a/src/scripts/store/selectors.js +++ b/src/scripts/store/selectors.js @@ -1,21 +1,26 @@ import { __ } from '@wordpress/i18n'; import { findContext } from './utils'; +/** + * @typedef {import('./index').Notice} Notice + * @typedef {import('./index').State} State + */ + /** * Fetch the rest api in order to get new notifications * - * @param {Object} state the current state - * @return {Object} the new notifications + * @param {State} state the current state + * @return {State} the new notifications */ export const fetchUpdates = ( state ) => state || {}; /** * Get the notices for the given context * - * @param {Object} state the current state - * @param {string} context the name of the list of notifications you want to retreive + * @param {State} state the current state + * @param {string} context the name of the list of notifications you want to retrieve * - * @return {Object[]} the list of notices of the context + * @return {Notice[]|Record} the list of notices of the context */ export const getNotices = ( state, context ) => { return context ? state[ context ] : state; @@ -25,8 +30,10 @@ export const getNotices = ( state, context ) => { * Adds a context to the current state. * commonly it's fired when the NotifyArea is registered * - * @param {Object} state the current state + * @param {State} state the current state * @param {string} context the context to add + * + * @return {State} the notice store state */ export const registerContext = ( state, context ) => { if ( ! state[ context ] ) { @@ -38,11 +45,11 @@ export const registerContext = ( state, context ) => { /** * It searches the Redux store for a notification by ID or by a search term * - * @param {Object} state - the current state + * @param {State} state - the current state * @param {string|number} searchTerm - The term you want to search for. * @param {?Object|Array} [args] - search args * - * @return {Object} the search result + * @return {Notice|Notice[]|string} the search result */ export const findNotice = ( state, searchTerm, args = { term: 'source' } ) => { // return the notification by id diff --git a/src/scripts/store/utils.js b/src/scripts/store/utils.js index efb52a04..21603bcf 100644 --- a/src/scripts/store/utils.js +++ b/src/scripts/store/utils.js @@ -1,7 +1,10 @@ +/** + * @typedef {import('./index').State} State + */ /** * Find the context for the given notification key. * - * @param {Object} notifications - The notifications object to search in + * @param {State} notifications - The notifications object to search in * @param {number} id - The notification id to search */ export function findContext( notifications, id ) { diff --git a/src/scripts/utils/drawer.js b/src/scripts/utils/drawer.js new file mode 100644 index 00000000..929be5f2 --- /dev/null +++ b/src/scripts/utils/drawer.js @@ -0,0 +1,92 @@ +import { dispatch } from '@wordpress/data'; +import { WEEK_IN_SECONDS } from '../components/NoticesArea'; +import { NOTIFY_NAMESPACE } from '../store/constants'; + +/** + * @typedef {import('../store').Notice} Notice + */ + +/** + * It clears the notices in the selected context + * + * @param {string} context - The context of the notices. This is used to determine which notices to clear. + */ +export const clearNotifyDrawer = ( context ) => { + dispatch( NOTIFY_NAMESPACE ).clear( context ); +}; + +/** + * At the moment the function return the notifications if the split by isn't set to "date" + * + * @param {Notice[]} notifications + * @param {string} by + * + * @return {Notice[][]|Notice[]} two list of Notifications, one for the new and one for the old + */ +export const getSorted = ( notifications, by = 'date' ) => { + const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; + if ( Limit ) { + return notifications.reduce( + ( [ current, past ], item ) => { + return item.date >= Limit + ? [ [ ...current, item ], past ] + : [ current, [ ...past, item ] ]; + }, + [ [], [] ] + ); + } + return notifications; +}; + +export const wpNotifyHub = document.getElementById( 'wp-admin-bar-wp-notify' ); + +/** + * When the user clicks on the notification drawer, the drawer is disabled + * + * @param {Event} e + * @callback {disableNotifyDrawer} disableNotifyDrawer + */ +export const disableNotifyDrawer = ( e ) => { + e.stopPropagation(); + wpNotifyHub.classList.remove( 'active' ); + document.onkeydown = null; + document.body.removeEventListener( 'click', disableNotifyDrawer ); +}; + +/** + * Enable the notification drawer + * If the notification drawer is not active, add the active class to the notification drawer and add an event listener to the body to disable the notification drawer + * The first thing we do is stop the propagation of the event. This is important because we don't want the event to bubble up to the body and trigger the disableNotifyDrawer function + * + * @callback {enableNotifyDrawer} enableNotifyDrawer + * @param {Event} e - The event object. + */ +export const enableNotifyDrawer = ( e ) => { + e.stopPropagation(); + if ( ! wpNotifyHub.classList.contains( 'active' ) ) { + wpNotifyHub.classList.add( 'active' ); + document.body.addEventListener( 'click', disableNotifyDrawer ); + document.onkeydown = ( ev ) => { + if ( 'key' in ev && ( ev.key === 'Escape' || ev.key === 'Esc' ) ) { + disableNotifyDrawer( e ); + } + }; + } +}; + +/** + * Action handler for the notification drawer + */ +if ( wpNotifyHub ) { + /** + * Notification hub + * Handle click on wp-admin bar bell icon that show the WP-Notify sidebar + * + * @member {HTMLElement} wpNotifyHub - the Notification Hub Controller + * @event enableNotifyDrawer - When the user clicks or focus on the notification drawer, the drawer is enabled + * @event disableNotifyDrawer - on focus out + */ + wpNotifyHub.addEventListener( 'click', enableNotifyDrawer ); + wpNotifyHub.addEventListener( 'focus', enableNotifyDrawer, true ); + wpNotifyHub.addEventListener( 'blur', disableNotifyDrawer, true ); +} diff --git a/src/scripts/wp-notify.js b/src/scripts/wp-notify.js index a2018985..ce4c6639 100644 --- a/src/scripts/wp-notify.js +++ b/src/scripts/wp-notify.js @@ -9,6 +9,9 @@ import Drawer from './components/Drawer'; /** The store default data */ import { NOTIFY_NAMESPACE, contexts } from './store/constants'; +/** + * @typedef {import('./store').Notice} Notice + */ /** * The redux store */ @@ -28,7 +31,7 @@ const notify = { /** * List all notifications or those of a particular context * - * @param {string|false} context + * @param {string} context */ get: ( context = '' ) => select( NOTIFY_NAMESPACE ).getNotices( context ), @@ -49,7 +52,7 @@ const notify = { /** * Add a new notification * - * @param {Object} payload + * @param {Notice} payload */ add: ( payload ) => dispatch( NOTIFY_NAMESPACE ).addNotice( payload ), @@ -63,7 +66,7 @@ const notify = { /** * Clear all notifications * - * @param {string|false} context + * @param {string} context */ clear: ( context = 'adminbar' ) => dispatch( NOTIFY_NAMESPACE ).clear( context ), From bfa8154eddb14e36d799de7c3b24bb484e6630aa Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 16:00:55 -0700 Subject: [PATCH 10/52] fix: stash issues --- src/scripts/store/index.js | 13 ------------- src/scripts/utils/effects.js | 9 ++++++--- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/scripts/store/index.js b/src/scripts/store/index.js index 3ebb7061..7a49aed6 100644 --- a/src/scripts/store/index.js +++ b/src/scripts/store/index.js @@ -8,8 +8,6 @@ import * as controls from './controls'; import * as resolvers from './resolvers'; /** - <<<<<<< Updated upstream - ======= * * @typedef {import('@wordpress/data/build-types/types').ReduxStoreConfig} StoreConfig * @typedef {import('@wordpress/data/build-types/types').StoreDescriptor} NoticeStore @@ -26,7 +24,6 @@ import * as resolvers from './resolvers'; */ /** - >>>>>>> Stashed changes * * @typedef {Object} DashiconsIcon The Dashicons icon type. * @property {string} dashicons The Dashicons slug of the icon. @@ -53,12 +50,7 @@ import * as resolvers from './resolvers'; */ /** - <<<<<<< Updated upstream - * - * @typedef {Object} Notification The notification type. - ======= * @typedef {Object} Notice The notification type. - >>>>>>> Stashed changes * @property {NoticeAction=} action The optional action associated to the notification. * @property {string=} context The rendering context of the notification. * @property {number} date The datetime from which the notification was emitted. @@ -72,12 +64,7 @@ import * as resolvers from './resolvers'; */ /** - <<<<<<< Updated upstream - * - * @typedef {Record} State The notifications redux store type. - ======= * @typedef {Record} State The notifications redux store type. - >>>>>>> Stashed changes */ /** diff --git a/src/scripts/utils/effects.js b/src/scripts/utils/effects.js index 566cadc9..c231e091 100644 --- a/src/scripts/utils/effects.js +++ b/src/scripts/utils/effects.js @@ -2,6 +2,9 @@ import { WEEK_IN_SECONDS } from '../components/NoticesArea'; import { dispatch } from '@wordpress/data'; import { NOTIFY_NAMESPACE } from '../store/constants'; +/** + * @typedef {import('../store').Notice} Notice + */ /** * Delay returns a promise that resolves after the specified number of milliseconds. * @@ -14,10 +17,10 @@ export const delay = ( ms ) => new Promise( ( f ) => setTimeout( f, ms ) ); /** * At the moment the function return the notifications if the split by isn't set to "date" * - * @param {Array} notifications - * @param {string} by + * @param {Notice[]} notifications + * @param {string} by * - * @return {Array} two list of Notifications, one for the new and one for the old + * @return {Notice[][]|Notice[]} two list of Notifications, one for the new and one for the old */ export const getSorted = ( notifications, by = 'date' ) => { const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; From 8c20c4d057bb06cbe41d338487302796265a14f9 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 16:34:25 -0700 Subject: [PATCH 11/52] feature: type redux store (#9) * feature: type store * fix: stash issues --- jsconfig.json | 2 +- src/index.d.ts | 11 ++++ src/scripts/store/actions.js | 54 +++++++++++++++++--- src/scripts/store/constants.js | 5 +- src/scripts/store/controls.js | 7 ++- src/scripts/store/index.js | 39 ++++++++++---- src/scripts/store/reducer.js | 12 +++-- src/scripts/store/selectors.js | 23 ++++++--- src/scripts/store/utils.js | 5 +- src/scripts/utils/drawer.js | 92 ++++++++++++++++++++++++++++++++++ src/scripts/utils/effects.js | 9 ++-- src/scripts/wp-notify.js | 9 ++-- 12 files changed, 227 insertions(+), 41 deletions(-) create mode 100644 src/index.d.ts create mode 100644 src/scripts/utils/drawer.js diff --git a/jsconfig.json b/jsconfig.json index c8d11b3a..1e783f0a 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "checkJs": true, - "types": [ "jest" ], + "types": [ "./src/index.d.ts", "jest" ], "baseUrl": "./src" }, "include": [ "./src/**/*.js", "./src/**/*.jsx" ], diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 00000000..db8ccf00 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,11 @@ +import { CurriedSelectorsOf } from '@wordpress/data/build-types/types' +import { NoticeStore, NOTIFY_NAMESPACE } from './scripts/store' + +declare global { + interface Window { wp: { notify: any }; wp_notify_data?: { settingsPage: string } } +} + +declare module '@wordpress/data' { + function dispatch( key: NOTIFY_NAMESPACE ): typeof import( './scripts/store/actions' ); + function select( key: NOTIFY_NAMESPACE ): CurriedSelectorsOf< NoticeStore >; +} diff --git a/src/scripts/store/actions.js b/src/scripts/store/actions.js index 22a61613..bf93c8b9 100644 --- a/src/scripts/store/actions.js +++ b/src/scripts/store/actions.js @@ -1,41 +1,83 @@ +/* eslint-disable jsdoc/require-returns-type */ + +/** + * @typedef {import('./index').Notice} Notice + */ + +/** + * Action creator to hydrate a notice from the store. + * + * @param {Notice[]} payload The notices to hydrate. + * @return A redux action. + */ export const hydrate = ( payload ) => { return { - type: 'HYDRATE', + type: /** @type {'HYDRATE'} */ ( 'HYDRATE' ), payload, }; }; +/** + * Action creator to clear a notification context from the store. + * + * @param {string} context The slug of the context to clear. + * @return A redux action. + */ export const clear = ( context ) => { return { - type: 'CLEAR', + type: /** @type {'CLEAR'} */ ( 'CLEAR' ), context, }; }; +/** + * Action creator to add a notice to the store. + * + * @param {Notice} payload The notice to add. + * @return A redux action. + */ export const addNotice = ( payload ) => { return { - type: 'ADD', + type: /** @type {'ADD'} */ ( 'ADD' ), payload, }; }; +/** + * Action creator to remove a notice from the store. + * + * @param {number} id The id of the notice to remove. + * @return A redux action. + */ export const removeNotice = ( id ) => { return { - type: 'DELETE', + type: /** @type {'DELETE'} */ ( 'DELETE' ), id, }; }; +/** + * Action creator to update a notice in the store. + * + * @param {Notice} payload + * @return A redux action. + */ export const updateNotice = ( payload ) => { return { - type: 'UPDATE', + type: /** @type {'UPDATE'} */ ( 'UPDATE' ), payload, }; }; +/** + * Action creator to fetch notices. + * + * @param {string} path The REST API route from which to fetch notices. + * @return A redux action. + */ export const fetchAPI = ( path = '' ) => { return { - type: 'FETCH', + type: /** @type {'FETCH'} */ ( 'FETCH' ), path, }; }; diff --git a/src/scripts/store/constants.js b/src/scripts/store/constants.js index a8c85dba..82bc2331 100644 --- a/src/scripts/store/constants.js +++ b/src/scripts/store/constants.js @@ -1,4 +1,3 @@ -/* global wp_notify_data */ /** * @member {string} NOTIFY_NAMESPACE WP-Notify namespace */ @@ -24,4 +23,6 @@ export const contexts = [ defaultContext, 'dashboard' ]; */ export const settingsPageUrl = // eslint-disable-next-line camelcase - typeof wp_notify_data !== 'undefined' ? wp_notify_data?.settingsPage : ''; + typeof window.wp_notify_data !== 'undefined' + ? window.wp_notify_data?.settingsPage + : ''; diff --git a/src/scripts/store/controls.js b/src/scripts/store/controls.js index 7c7881e3..1ae131b6 100644 --- a/src/scripts/store/controls.js +++ b/src/scripts/store/controls.js @@ -1,11 +1,14 @@ import apiFetch from '@wordpress/api-fetch'; import { API_PATH } from './constants'; +/** + * @typedef {import('./index').Notice} Notice + */ /** * Fetches the wp-notify rest api endpoint for the specified endpoint * - * @param {string} action - the action to execute - * @return {Promise} - the Promise with the results + * @param {{path: string}} action The action to execute + * @return {Promise} The Promise with the results */ export const FETCH = ( action ) => { return apiFetch( { diff --git a/src/scripts/store/index.js b/src/scripts/store/index.js index 8126c473..7a49aed6 100644 --- a/src/scripts/store/index.js +++ b/src/scripts/store/index.js @@ -8,6 +8,23 @@ import * as controls from './controls'; import * as resolvers from './resolvers'; /** + * + * @typedef {import('@wordpress/data/build-types/types').ReduxStoreConfig} StoreConfig + * @typedef {import('@wordpress/data/build-types/types').StoreDescriptor} NoticeStore + */ + +/** + * @template {Record} T + * @template {keyof T} K + * @typedef {T[K]} ValuesOf + */ + +/** + * @typedef {ReturnType>} Action + */ + +/** + * * @typedef {Object} DashiconsIcon The Dashicons icon type. * @property {string} dashicons The Dashicons slug of the icon. */ @@ -33,7 +50,7 @@ import * as resolvers from './resolvers'; */ /** - * @typedef {Object} Notification The notification type. + * @typedef {Object} Notice The notification type. * @property {NoticeAction=} action The optional action associated to the notification. * @property {string=} context The rendering context of the notification. * @property {number} date The datetime from which the notification was emitted. @@ -47,21 +64,23 @@ import * as resolvers from './resolvers'; */ /** - * @typedef {Record} State The notifications redux store type. + * @typedef {Record} State The notifications redux store type. */ /** * Creating a store for the redux state. * - * @return {store} A Redux store that lets you read the state, dispatch actions and subscribe to changes. + * A Redux store that lets you read the state, dispatch actions and subscribe to changes. */ -const store = createReduxStore( NOTIFY_NAMESPACE, { - reducer, - actions, - selectors, - controls, - resolvers, -} ); +const store = /** @type {NoticeStore} */ ( + createReduxStore( NOTIFY_NAMESPACE, { + reducer, + actions, + selectors, + controls, + resolvers, + } ) +); register( store ); diff --git a/src/scripts/store/reducer.js b/src/scripts/store/reducer.js index 0c70579f..52a9f040 100644 --- a/src/scripts/store/reducer.js +++ b/src/scripts/store/reducer.js @@ -1,13 +1,16 @@ import { findContext } from './utils'; +/** + * @typedef {import('redux').Reducer} NoticeReducer + * @typedef {import('./index').State} State + * @typedef {import('./index').Action} Action + */ + /** * Reducer returning the next notices state. The notices state is an object * where each key is a context, its value an array of notice objects. * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. + * @type {NoticeReducer} */ const reducer = ( state = {}, action ) => { switch ( action.type ) { @@ -21,7 +24,6 @@ const reducer = ( state = {}, action ) => { }; } ); return updated; - case 'ADD': return { ...state, diff --git a/src/scripts/store/selectors.js b/src/scripts/store/selectors.js index 959c6e3f..455b6922 100644 --- a/src/scripts/store/selectors.js +++ b/src/scripts/store/selectors.js @@ -1,21 +1,26 @@ import { __ } from '@wordpress/i18n'; import { findContext } from './utils'; +/** + * @typedef {import('./index').Notice} Notice + * @typedef {import('./index').State} State + */ + /** * Fetch the rest api in order to get new notifications * - * @param {Object} state the current state - * @return {Object} the new notifications + * @param {State} state the current state + * @return {State} the new notifications */ export const fetchUpdates = ( state ) => state || {}; /** * Get the notices for the given context * - * @param {Object} state the current state - * @param {string} context the name of the list of notifications you want to retreive + * @param {State} state the current state + * @param {string} context the name of the list of notifications you want to retrieve * - * @return {Object[]} the list of notices of the context + * @return {Notice[]|Record} the list of notices of the context */ export const getNotices = ( state, context ) => { return context ? state[ context ] : state; @@ -25,8 +30,10 @@ export const getNotices = ( state, context ) => { * Adds a context to the current state. * commonly it's fired when the NotifyArea is registered * - * @param {Object} state the current state + * @param {State} state the current state * @param {string} context the context to add + * + * @return {State} the notice store state */ export const registerContext = ( state, context ) => { if ( ! state[ context ] ) { @@ -38,11 +45,11 @@ export const registerContext = ( state, context ) => { /** * It searches the Redux store for a notification by ID or by a search term * - * @param {Object} state - the current state + * @param {State} state - the current state * @param {string|number} searchTerm - The term you want to search for. * @param {?Object|Array} [args] - search args * - * @return {Object} the search result + * @return {Notice|Notice[]|string} the search result */ export const findNotice = ( state, searchTerm, args = { term: 'source' } ) => { // return the notification by id diff --git a/src/scripts/store/utils.js b/src/scripts/store/utils.js index efb52a04..21603bcf 100644 --- a/src/scripts/store/utils.js +++ b/src/scripts/store/utils.js @@ -1,7 +1,10 @@ +/** + * @typedef {import('./index').State} State + */ /** * Find the context for the given notification key. * - * @param {Object} notifications - The notifications object to search in + * @param {State} notifications - The notifications object to search in * @param {number} id - The notification id to search */ export function findContext( notifications, id ) { diff --git a/src/scripts/utils/drawer.js b/src/scripts/utils/drawer.js new file mode 100644 index 00000000..929be5f2 --- /dev/null +++ b/src/scripts/utils/drawer.js @@ -0,0 +1,92 @@ +import { dispatch } from '@wordpress/data'; +import { WEEK_IN_SECONDS } from '../components/NoticesArea'; +import { NOTIFY_NAMESPACE } from '../store/constants'; + +/** + * @typedef {import('../store').Notice} Notice + */ + +/** + * It clears the notices in the selected context + * + * @param {string} context - The context of the notices. This is used to determine which notices to clear. + */ +export const clearNotifyDrawer = ( context ) => { + dispatch( NOTIFY_NAMESPACE ).clear( context ); +}; + +/** + * At the moment the function return the notifications if the split by isn't set to "date" + * + * @param {Notice[]} notifications + * @param {string} by + * + * @return {Notice[][]|Notice[]} two list of Notifications, one for the new and one for the old + */ +export const getSorted = ( notifications, by = 'date' ) => { + const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; + if ( Limit ) { + return notifications.reduce( + ( [ current, past ], item ) => { + return item.date >= Limit + ? [ [ ...current, item ], past ] + : [ current, [ ...past, item ] ]; + }, + [ [], [] ] + ); + } + return notifications; +}; + +export const wpNotifyHub = document.getElementById( 'wp-admin-bar-wp-notify' ); + +/** + * When the user clicks on the notification drawer, the drawer is disabled + * + * @param {Event} e + * @callback {disableNotifyDrawer} disableNotifyDrawer + */ +export const disableNotifyDrawer = ( e ) => { + e.stopPropagation(); + wpNotifyHub.classList.remove( 'active' ); + document.onkeydown = null; + document.body.removeEventListener( 'click', disableNotifyDrawer ); +}; + +/** + * Enable the notification drawer + * If the notification drawer is not active, add the active class to the notification drawer and add an event listener to the body to disable the notification drawer + * The first thing we do is stop the propagation of the event. This is important because we don't want the event to bubble up to the body and trigger the disableNotifyDrawer function + * + * @callback {enableNotifyDrawer} enableNotifyDrawer + * @param {Event} e - The event object. + */ +export const enableNotifyDrawer = ( e ) => { + e.stopPropagation(); + if ( ! wpNotifyHub.classList.contains( 'active' ) ) { + wpNotifyHub.classList.add( 'active' ); + document.body.addEventListener( 'click', disableNotifyDrawer ); + document.onkeydown = ( ev ) => { + if ( 'key' in ev && ( ev.key === 'Escape' || ev.key === 'Esc' ) ) { + disableNotifyDrawer( e ); + } + }; + } +}; + +/** + * Action handler for the notification drawer + */ +if ( wpNotifyHub ) { + /** + * Notification hub + * Handle click on wp-admin bar bell icon that show the WP-Notify sidebar + * + * @member {HTMLElement} wpNotifyHub - the Notification Hub Controller + * @event enableNotifyDrawer - When the user clicks or focus on the notification drawer, the drawer is enabled + * @event disableNotifyDrawer - on focus out + */ + wpNotifyHub.addEventListener( 'click', enableNotifyDrawer ); + wpNotifyHub.addEventListener( 'focus', enableNotifyDrawer, true ); + wpNotifyHub.addEventListener( 'blur', disableNotifyDrawer, true ); +} diff --git a/src/scripts/utils/effects.js b/src/scripts/utils/effects.js index 566cadc9..c231e091 100644 --- a/src/scripts/utils/effects.js +++ b/src/scripts/utils/effects.js @@ -2,6 +2,9 @@ import { WEEK_IN_SECONDS } from '../components/NoticesArea'; import { dispatch } from '@wordpress/data'; import { NOTIFY_NAMESPACE } from '../store/constants'; +/** + * @typedef {import('../store').Notice} Notice + */ /** * Delay returns a promise that resolves after the specified number of milliseconds. * @@ -14,10 +17,10 @@ export const delay = ( ms ) => new Promise( ( f ) => setTimeout( f, ms ) ); /** * At the moment the function return the notifications if the split by isn't set to "date" * - * @param {Array} notifications - * @param {string} by + * @param {Notice[]} notifications + * @param {string} by * - * @return {Array} two list of Notifications, one for the new and one for the old + * @return {Notice[][]|Notice[]} two list of Notifications, one for the new and one for the old */ export const getSorted = ( notifications, by = 'date' ) => { const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; diff --git a/src/scripts/wp-notify.js b/src/scripts/wp-notify.js index a2018985..ce4c6639 100644 --- a/src/scripts/wp-notify.js +++ b/src/scripts/wp-notify.js @@ -9,6 +9,9 @@ import Drawer from './components/Drawer'; /** The store default data */ import { NOTIFY_NAMESPACE, contexts } from './store/constants'; +/** + * @typedef {import('./store').Notice} Notice + */ /** * The redux store */ @@ -28,7 +31,7 @@ const notify = { /** * List all notifications or those of a particular context * - * @param {string|false} context + * @param {string} context */ get: ( context = '' ) => select( NOTIFY_NAMESPACE ).getNotices( context ), @@ -49,7 +52,7 @@ const notify = { /** * Add a new notification * - * @param {Object} payload + * @param {Notice} payload */ add: ( payload ) => dispatch( NOTIFY_NAMESPACE ).addNotice( payload ), @@ -63,7 +66,7 @@ const notify = { /** * Clear all notifications * - * @param {string|false} context + * @param {string} context */ clear: ( context = 'adminbar' ) => dispatch( NOTIFY_NAMESPACE ).clear( context ), From 098d52040ba1abb3baa75ddc2ba9c81c23433ddc Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 23:22:06 -0700 Subject: [PATCH 12/52] feature: type components --- src/index.d.ts => index.d.ts | 4 ++-- src/scripts/components/Notice.js | 33 +++++++++++++++++++------- src/scripts/components/NoticeAction.js | 10 +++++++- src/scripts/components/NoticeEmpty.js | 5 ++++ src/scripts/components/NoticeImage.js | 20 ++++++++++------ src/scripts/components/NoticesArea.js | 23 +++++++++++++----- src/scripts/components/NoticesLoop.js | 8 +++---- src/scripts/store/actions.js | 2 +- src/scripts/store/index.js | 29 +++++++++++++--------- src/scripts/store/selectors.js | 4 ++-- src/scripts/utils/effects.js | 11 ++++++--- src/scripts/utils/guards.js | 30 +++++++++++++++++++++++ src/scripts/utils/metaBox.js | 28 ++++++++++++++-------- src/scripts/utils/sanitization.js | 1 + 14 files changed, 152 insertions(+), 56 deletions(-) rename src/index.d.ts => index.d.ts (78%) create mode 100644 src/scripts/utils/guards.js diff --git a/src/index.d.ts b/index.d.ts similarity index 78% rename from src/index.d.ts rename to index.d.ts index db8ccf00..c0db94ef 100644 --- a/src/index.d.ts +++ b/index.d.ts @@ -1,11 +1,11 @@ import { CurriedSelectorsOf } from '@wordpress/data/build-types/types' -import { NoticeStore, NOTIFY_NAMESPACE } from './scripts/store' +import { NoticeStore, NOTIFY_NAMESPACE } from './src/scripts/store' declare global { interface Window { wp: { notify: any }; wp_notify_data?: { settingsPage: string } } } declare module '@wordpress/data' { - function dispatch( key: NOTIFY_NAMESPACE ): typeof import( './scripts/store/actions' ); + function dispatch( key: NOTIFY_NAMESPACE ): typeof import( './src/scripts/store/actions' ); function select( key: NOTIFY_NAMESPACE ): CurriedSelectorsOf< NoticeStore >; } diff --git a/src/scripts/components/Notice.js b/src/scripts/components/Notice.js index 5a854847..efe4dd22 100644 --- a/src/scripts/components/Notice.js +++ b/src/scripts/components/Notice.js @@ -15,6 +15,14 @@ import moment from 'moment'; import { defaultContext, NOTIFY_NAMESPACE } from '../store/constants'; import { dispatch } from '@wordpress/data'; +/** + * @typedef {import('../store').Notice} Notice + */ +/** + * @param {Object} props + * @param {number} props.date The date of the notification. + * @param {string} props.source The source of the notification. + */ export const NoticeMeta = ( { date, source } ) => (

{ source } { '\u2022 ' } @@ -25,20 +33,23 @@ export const NoticeMeta = ( { date, source } ) => ( /** * It renders a single notice * - * @param {Object} props + * @param {Notice} props * @return {JSX.Element} Notice - the single notice */ export const Notice = ( props ) => { const { - id, - title, - status, + action, context = defaultContext, - source = 'WordPress', date = Date.now() * 0.001, + dismissible, + icon, + id, message, severity, - action, + source = 'WordPress', + status, + title, + unread, } = props; /** @@ -57,9 +68,9 @@ export const Notice = ( props ) => { className={ classnames( 'wp-notification', 'wp-notice-' + id, - action?.dismissible ? 'dismissible' : null, + dismissible ? 'dismissible' : null, severity ? severity : null, - action?.unread ? 'unread' : null, + unread ? 'unread' : null, status ) } > @@ -76,7 +87,11 @@ export const Notice = ( props ) => { - + ); }; diff --git a/src/scripts/components/NoticeAction.js b/src/scripts/components/NoticeAction.js index d819308f..e2ed866c 100644 --- a/src/scripts/components/NoticeAction.js +++ b/src/scripts/components/NoticeAction.js @@ -2,11 +2,19 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { defaultContext } from '../store/constants'; +/** + * @typedef {Object} Action + * @property {string=} acceptLink The url of the action. + * @property {string=} acceptMessage The label of the action. + * @property {string=} dismissLabel The label of the dismiss action. + * @property {boolean=} dismissible Predicate of whether or not the notification can be dismissed. + */ + /** * Renders an image or icon based on the type of notification * * @param {Object} param - * @param {Object} param.action + * @param {Action} param.action * @param {string} param.context * @param {Function} param.onDismiss - callback to be called when the notice is dismissed * @return {JSX.Element} NoticeImage - the image or the icon wrapped into a div diff --git a/src/scripts/components/NoticeEmpty.js b/src/scripts/components/NoticeEmpty.js index 68725dca..98613fb7 100644 --- a/src/scripts/components/NoticeEmpty.js +++ b/src/scripts/components/NoticeEmpty.js @@ -1,5 +1,10 @@ import { comment, Icon } from '@wordpress/icons'; +/** + * @param {Object} props + * @param {number} props.size The size of the icon. + * @param {string} props.message The message of the notification. + */ export const NoticeEmpty = ( props ) => { return (

diff --git a/src/scripts/components/NoticeImage.js b/src/scripts/components/NoticeImage.js index ed0ad391..fdeecd30 100644 --- a/src/scripts/components/NoticeImage.js +++ b/src/scripts/components/NoticeImage.js @@ -1,30 +1,36 @@ +// @ts-ignore import wpLogo from '../../images/WordPressLogo.svg'; import classNames from 'classnames'; import { defaultContext } from '../store/constants'; import { purify } from '../utils/sanitization'; +import { isDashiconsIcon, isSvgIcon } from '../utils/guards'; + +/** + * @typedef {import('../store').NoticeIcon} NoticeIcon + */ /** * It returns a div with a class name of `wp-notification-image` and `wp-notification-` plus the type of image passed in * an image or a svg element depending on the type of image passed in * - * @param {Object} props - The props object is a JavaScript object that contains all the properties that were passed to the component. - * @param {Object} props.icon - the Object containing the notification icon properties - * @param {string} props.context - the location of the notification - * @param {string} props.severity + * @param {Object} props - The props object is a JavaScript object that contains all the properties that were passed to the component. + * @param {NoticeIcon} props.icon - the Object containing the notification icon properties + * @param {string} props.context - the location of the notification + * @param {string=} props.severity * @return {JSX.Element} A div with a className of 'wp-notification-image' and 'wp-notification-' + props.type. */ export const NoticeIcon = ( { icon, severity, context = defaultContext } ) => { /** build the notice container css classes definition */ const classes = classNames( 'wp-notification-image', - ! icon?.dashicons || 'wp-notification-icon', + ! isDashiconsIcon( icon ) || 'wp-notification-icon', 'wp-notification-' + ( context || 'adminbar' ) ); let image; // TODO: maybe is better to have a default definition like {type: "svg"} ? - if ( icon?.svg ) { + if ( isSvgIcon( icon ) ) { /** Since we don't want to double wrap svg's we need to return the div immediately */ return (
{ dangerouslySetInnerHTML={ purify( icon?.svg ) } /> ); - } else if ( icon?.dashicons ) { + } else if ( isDashiconsIcon( icon ) ) { image = ( component with the updated state * - * @param {Object} props + * @param {Props} props * @return {JSX.Element} Notifications */ export const NoticesArea = ( props ) => { @@ -23,10 +37,7 @@ export const NoticesArea = ( props ) => { /* * Todo: this method should supply to rest api the user data, current page, moreover the request args may be added (notice per page, notice filters and sort) */ - notifications = useSelect( - ( select ) => select( NOTIFY_NAMESPACE ).getNotices( context ), - [] - ); + notifications = useSelect( store, [] ).getNotices( context ); /** * if the context is the adminbar we need to render a list of notifications with the recent notifications and the old notifications diff --git a/src/scripts/components/NoticesLoop.js b/src/scripts/components/NoticesLoop.js index ca8bdfb0..d23bcfab 100644 --- a/src/scripts/components/NoticesLoop.js +++ b/src/scripts/components/NoticesLoop.js @@ -4,10 +4,10 @@ import { Notice } from './Notice'; * Returns a list of notices * each notice is a component that has a key, an id, an image, an additional class name, and an onDismiss function * - * @param {Object} prop - * @param {Array} prop.notices - An array of objects that contain the notice data. + * @param {Object} prop + * @param {import('../store').Notice[]} prop.notices - An array of objects that contain the notice data. * - * @return {Array} An array of Notice components. + * @return {JSX.Element} An array of Notice components. */ export const NoticesLoop = ( { notices } ) => - notices.map( ( notify ) => ); + notices.map( ( notice ) => ); diff --git a/src/scripts/store/actions.js b/src/scripts/store/actions.js index bf93c8b9..f4b91248 100644 --- a/src/scripts/store/actions.js +++ b/src/scripts/store/actions.js @@ -59,7 +59,7 @@ export const removeNotice = ( id ) => { /** * Action creator to update a notice in the store. * - * @param {Notice} payload + * @param {Pick|Partial} payload * @return A redux action. */ export const updateNotice = ( payload ) => { diff --git a/src/scripts/store/index.js b/src/scripts/store/index.js index 7a49aed6..debf13a5 100644 --- a/src/scripts/store/index.js +++ b/src/scripts/store/index.js @@ -46,21 +46,28 @@ import * as resolvers from './resolvers'; /** * @typedef {Object} NoticeAction The notification action type. * @property {string} acceptLink The url of the action. - * @property {string} acceptMessage The message content of the action. + * @property {string} acceptMessage The label of the action. + */ + +/** + * @typedef {'dismissing'} NoticeStatus The notification status type. */ /** * @typedef {Object} Notice The notification type. - * @property {NoticeAction=} action The optional action associated to the notification. - * @property {string=} context The rendering context of the notification. - * @property {number} date The datetime from which the notification was emitted. - * @property {boolean} dismissible Predicate of whether or not the notification can be dismissed. - * @property {NoticeIcon=} icon The optional icon. - * @property {number} id The database id of the notification message. - * @property {string=} message The message content of the notification. - * @property {string=} source The source of the notification. - * @property {string} title The title of the notification message. - * @property {boolean} unread Predicate of whether or not the notification is in an unread state. + * @property {NoticeAction=} action The optional action associated to the notification. + * @property {string=} context The rendering context of the notification. + * @property {number} date The datetime from which the notification was emitted. + * @property {string=} dismissLabel The label of the dismiss action. + * @property {boolean=} dismissible Predicate of whether or not the notification can be dismissed. + * @property {NoticeIcon=} icon The optional icon. + * @property {number} id The database id of the notification message. + * @property {string=} message The message content of the notification. + * @property {string=} severity The severity of the notification. + * @property {string=} source The source of the notification. + * @property {NoticeStatus=} status The status of the notification. + * @property {string} title The title of the notification message. + * @property {boolean=} unread Predicate of whether or not the notification is in an unread state. */ /** diff --git a/src/scripts/store/selectors.js b/src/scripts/store/selectors.js index 455b6922..498a7307 100644 --- a/src/scripts/store/selectors.js +++ b/src/scripts/store/selectors.js @@ -20,10 +20,10 @@ export const fetchUpdates = ( state ) => state || {}; * @param {State} state the current state * @param {string} context the name of the list of notifications you want to retrieve * - * @return {Notice[]|Record} the list of notices of the context + * @return {Notice[]} the list of notices of the context */ export const getNotices = ( state, context ) => { - return context ? state[ context ] : state; + return state[ context ]; }; /** diff --git a/src/scripts/utils/effects.js b/src/scripts/utils/effects.js index c231e091..2c1bd95f 100644 --- a/src/scripts/utils/effects.js +++ b/src/scripts/utils/effects.js @@ -5,6 +5,11 @@ import { NOTIFY_NAMESPACE } from '../store/constants'; /** * @typedef {import('../store').Notice} Notice */ + +/** + * @typedef {'date'} SortBy + */ + /** * Delay returns a promise that resolves after the specified number of milliseconds. * @@ -18,9 +23,9 @@ export const delay = ( ms ) => new Promise( ( f ) => setTimeout( f, ms ) ); * At the moment the function return the notifications if the split by isn't set to "date" * * @param {Notice[]} notifications - * @param {string} by + * @param {SortBy} by * - * @return {Notice[][]|Notice[]} two list of Notifications, one for the new and one for the old + * @return {Notice[][]} two list of Notifications, one for the new and one for the old */ export const getSorted = ( notifications, by = 'date' ) => { const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; @@ -34,7 +39,7 @@ export const getSorted = ( notifications, by = 'date' ) => { [ [], [] ] ); } - return notifications; + return [ notifications ]; }; /** diff --git a/src/scripts/utils/guards.js b/src/scripts/utils/guards.js new file mode 100644 index 00000000..b0ab71cb --- /dev/null +++ b/src/scripts/utils/guards.js @@ -0,0 +1,30 @@ +/** + * @typedef {import('../store').DashiconsIcon} DashiconsIcon + * @typedef {import('../store').ImageIcon} ImageIcon + * @typedef {import('../store').SvgIcon} SvgIcon + * @typedef {import('../store').NoticeIcon} NoticeIcon + */ + +/** + * @param {Object} icon The icon to guard as a Dashicons icon. + * @return {icon is DashiconsIcon} Whether or not the icon is an Dashicons icon. + */ +export const isDashiconsIcon = ( icon ) => { + return typeof icon?.dashicons === 'string'; +}; + +/** + * @param {Object} icon The icon to guard as a image icon. + * @return {icon is ImageIcon} Whether or not the icon is an image icon. + */ +export const isImageIcon = ( icon ) => { + return typeof icon?.src === 'string'; +}; + +/** + * @param {Object} icon The icon to guard as a SVG icon. + * @return {icon is SvgIcon} Whether or not the icon is an SVG icon. + */ +export const isSvgIcon = ( icon ) => { + return typeof icon?.svg === 'string'; +}; diff --git a/src/scripts/utils/metaBox.js b/src/scripts/utils/metaBox.js index cf8547f9..8f726fa3 100644 --- a/src/scripts/utils/metaBox.js +++ b/src/scripts/utils/metaBox.js @@ -4,6 +4,10 @@ import { dispatch } from '@wordpress/data'; import { NOTIFY_NAMESPACE } from '../store/constants'; +/** + * @typedef {import('../store').Notice} Notice + */ + window.addEventListener( 'load', () => { /** * Adding an event listener to the form with the id of `wp-notification-metabox-form` that adds a new notification using the form data @@ -15,18 +19,22 @@ window.addEventListener( 'load', () => { if ( wpNotificationMetabox ) { wpNotificationMetabox.addEventListener( 'submit', ( e ) => { e.preventDefault(); - const title = document.getElementById( - 'wp-notification-metabox-form-title' + const title = /** @type {HTMLInputElement} */ ( + document.getElementById( 'wp-notification-metabox-form-title' ) ).value; - const message = document.getElementById( - 'wp-notification-metabox-form-message' + const message = /** @type {HTMLInputElement} */ ( + document.getElementById( + 'wp-notification-metabox-form-message' + ) ).value; - dispatch( NOTIFY_NAMESPACE ).addNotice( { - title, - message, - context: 'dashboard', - } ); + dispatch( NOTIFY_NAMESPACE ).addNotice( + /** @type {Notice} */ ( { + title, + message, + context: 'dashboard', + } ) + ); } ); } @@ -38,6 +46,6 @@ window.addEventListener( 'load', () => { ); if ( wpNotificationClearAll ) wpNotificationClearAll.addEventListener( 'click', () => { - wp.notify.clear( 'dashboard' ); + window.wp.notify.clear( 'dashboard' ); } ); } ); diff --git a/src/scripts/utils/sanitization.js b/src/scripts/utils/sanitization.js index abea8ebc..7bf3d1f8 100644 --- a/src/scripts/utils/sanitization.js +++ b/src/scripts/utils/sanitization.js @@ -5,6 +5,7 @@ import DOMPurify from 'dompurify'; * * @param {string} string - The text to be purified. * @param {Object} options - https://github.com/cure53/DOMPurify#can-i-configure-dompurify + * @return {{ __html: string }} The sanitized string. */ export const purify = ( string, options = {} ) => { return { From 9069727edbd4a3797dae9ad104083dfaf0eabdf7 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 23:35:41 -0700 Subject: [PATCH 13/52] fix: revert changes --- src/scripts/store/selectors.js | 4 ++-- src/scripts/utils/effects.js | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/scripts/store/selectors.js b/src/scripts/store/selectors.js index 498a7307..455b6922 100644 --- a/src/scripts/store/selectors.js +++ b/src/scripts/store/selectors.js @@ -20,10 +20,10 @@ export const fetchUpdates = ( state ) => state || {}; * @param {State} state the current state * @param {string} context the name of the list of notifications you want to retrieve * - * @return {Notice[]} the list of notices of the context + * @return {Notice[]|Record} the list of notices of the context */ export const getNotices = ( state, context ) => { - return state[ context ]; + return context ? state[ context ] : state; }; /** diff --git a/src/scripts/utils/effects.js b/src/scripts/utils/effects.js index 2c1bd95f..c231e091 100644 --- a/src/scripts/utils/effects.js +++ b/src/scripts/utils/effects.js @@ -5,11 +5,6 @@ import { NOTIFY_NAMESPACE } from '../store/constants'; /** * @typedef {import('../store').Notice} Notice */ - -/** - * @typedef {'date'} SortBy - */ - /** * Delay returns a promise that resolves after the specified number of milliseconds. * @@ -23,9 +18,9 @@ export const delay = ( ms ) => new Promise( ( f ) => setTimeout( f, ms ) ); * At the moment the function return the notifications if the split by isn't set to "date" * * @param {Notice[]} notifications - * @param {SortBy} by + * @param {string} by * - * @return {Notice[][]} two list of Notifications, one for the new and one for the old + * @return {Notice[][]|Notice[]} two list of Notifications, one for the new and one for the old */ export const getSorted = ( notifications, by = 'date' ) => { const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; @@ -39,7 +34,7 @@ export const getSorted = ( notifications, by = 'date' ) => { [ [], [] ] ); } - return [ notifications ]; + return notifications; }; /** From 1164a2700b05dbf2b00d76ee890c4ea198def976 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 23:36:34 -0700 Subject: [PATCH 14/52] fix: pass NoticeAction the correct props --- src/scripts/components/Notice.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scripts/components/Notice.js b/src/scripts/components/Notice.js index efe4dd22..c6a78827 100644 --- a/src/scripts/components/Notice.js +++ b/src/scripts/components/Notice.js @@ -41,6 +41,7 @@ export const Notice = ( props ) => { action, context = defaultContext, date = Date.now() * 0.001, + dismissLabel, dismissible, icon, id, @@ -80,7 +81,7 @@ export const Notice = ( props ) => {

) : null } From bd5802613553c6de5cedf732baf4e59cc2a89184 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 8 Apr 2023 12:49:05 +0200 Subject: [PATCH 15/52] notification Hub Component --- src/scripts/components/Drawer.js | 61 ++---------- src/scripts/components/NotificationHub.js | 47 ++++++++++ src/scripts/components/NotificationHubIcon.js | 15 +++ src/scripts/utils/drawer.js | 92 ------------------- src/scripts/utils/sanitization.js | 13 --- 5 files changed, 71 insertions(+), 157 deletions(-) create mode 100644 src/scripts/components/NotificationHub.js create mode 100644 src/scripts/components/NotificationHubIcon.js delete mode 100644 src/scripts/utils/drawer.js delete mode 100644 src/scripts/utils/sanitization.js diff --git a/src/scripts/components/Drawer.js b/src/scripts/components/Drawer.js index d4ebfa9d..20e0ce49 100644 --- a/src/scripts/components/Drawer.js +++ b/src/scripts/components/Drawer.js @@ -1,58 +1,15 @@ import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; - -export const wpNotifyDrawer = () => { - return ( -
-

- { __( 'Notifications' ) } -

-
-
- ); -}; - -export const wpNotifyHubIcon = () => { - return ( -
- - { __( 'Notifications' ) } -
- ); -}; +import { NoticesArea } from './NoticesArea'; export default () => { - const [ isActive, setIsActive ] = useState( false ); - - const toggleDrawer = () => { - setIsActive( ! isActive ); - }; - - const handleIconKeyDown = ( ev ) => { - if ( ( 'key' in ev && ev.key === 'Enter' ) || ev.key === ' ' ) { - // Activate item on "Enter" or "Space" key press - setIsActive( true ); - } - }; - - const handleDrawerKeyDown = ( ev ) => { - if ( 'key' in ev && ( ev.key === 'Escape' || ev.key === 'Esc' ) ) { - // Close the drawer on "Escape" or "Esc" key press - setIsActive( false ); - } - }; - return ( - <> - toggleDrawer( event ) } - onKeyDown={ ( event ) => handleIconKeyDown( event ) } - /> - - + ); }; diff --git a/src/scripts/components/NotificationHub.js b/src/scripts/components/NotificationHub.js new file mode 100644 index 00000000..04729a23 --- /dev/null +++ b/src/scripts/components/NotificationHub.js @@ -0,0 +1,47 @@ +import { __ } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; +import { + ShortcutProvider, + store as keyboardShortcutsStore, +} from '@wordpress/keyboard-shortcuts'; +import { useDispatch } from '@wordpress/data'; +import Drawer from './Drawer'; +import NotificationHubIcon from './NotificationHubIcon'; +import classNames from 'classnames'; + +export default () => { + /** Drawer state */ + const [ isActive, setIsActive ] = useState( false ); + + /** Register the keyboard shortcut(s) */ + const { registerShortcut } = useDispatch( keyboardShortcutsStore ); + useEffect( () => { + registerShortcut( { + name: 'wp-feature-notifications/close-drawer', + category: 'wp-feature-notifications', + description: __( 'Close the Notification drawer' ), + keyCombination: { + character: 'Escape', + }, + } ); + } ); + + function toggleDrawer() { + setIsActive( ! isActive ); + } + + return ( + + + + + ); +}; diff --git a/src/scripts/components/NotificationHubIcon.js b/src/scripts/components/NotificationHubIcon.js new file mode 100644 index 00000000..92f80ecd --- /dev/null +++ b/src/scripts/components/NotificationHubIcon.js @@ -0,0 +1,15 @@ +import { __ } from '@wordpress/i18n'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; + +export default ( { toggle, isActive } ) => { + useShortcut( 'wp-feature-notifications/close-drawer', () => { + if ( isActive ) toggle(); + } ); + + return ( + + ); +}; diff --git a/src/scripts/utils/drawer.js b/src/scripts/utils/drawer.js deleted file mode 100644 index 929be5f2..00000000 --- a/src/scripts/utils/drawer.js +++ /dev/null @@ -1,92 +0,0 @@ -import { dispatch } from '@wordpress/data'; -import { WEEK_IN_SECONDS } from '../components/NoticesArea'; -import { NOTIFY_NAMESPACE } from '../store/constants'; - -/** - * @typedef {import('../store').Notice} Notice - */ - -/** - * It clears the notices in the selected context - * - * @param {string} context - The context of the notices. This is used to determine which notices to clear. - */ -export const clearNotifyDrawer = ( context ) => { - dispatch( NOTIFY_NAMESPACE ).clear( context ); -}; - -/** - * At the moment the function return the notifications if the split by isn't set to "date" - * - * @param {Notice[]} notifications - * @param {string} by - * - * @return {Notice[][]|Notice[]} two list of Notifications, one for the new and one for the old - */ -export const getSorted = ( notifications, by = 'date' ) => { - const Limit = by === 'date' ? Date.now() - WEEK_IN_SECONDS : false; - if ( Limit ) { - return notifications.reduce( - ( [ current, past ], item ) => { - return item.date >= Limit - ? [ [ ...current, item ], past ] - : [ current, [ ...past, item ] ]; - }, - [ [], [] ] - ); - } - return notifications; -}; - -export const wpNotifyHub = document.getElementById( 'wp-admin-bar-wp-notify' ); - -/** - * When the user clicks on the notification drawer, the drawer is disabled - * - * @param {Event} e - * @callback {disableNotifyDrawer} disableNotifyDrawer - */ -export const disableNotifyDrawer = ( e ) => { - e.stopPropagation(); - wpNotifyHub.classList.remove( 'active' ); - document.onkeydown = null; - document.body.removeEventListener( 'click', disableNotifyDrawer ); -}; - -/** - * Enable the notification drawer - * If the notification drawer is not active, add the active class to the notification drawer and add an event listener to the body to disable the notification drawer - * The first thing we do is stop the propagation of the event. This is important because we don't want the event to bubble up to the body and trigger the disableNotifyDrawer function - * - * @callback {enableNotifyDrawer} enableNotifyDrawer - * @param {Event} e - The event object. - */ -export const enableNotifyDrawer = ( e ) => { - e.stopPropagation(); - if ( ! wpNotifyHub.classList.contains( 'active' ) ) { - wpNotifyHub.classList.add( 'active' ); - document.body.addEventListener( 'click', disableNotifyDrawer ); - document.onkeydown = ( ev ) => { - if ( 'key' in ev && ( ev.key === 'Escape' || ev.key === 'Esc' ) ) { - disableNotifyDrawer( e ); - } - }; - } -}; - -/** - * Action handler for the notification drawer - */ -if ( wpNotifyHub ) { - /** - * Notification hub - * Handle click on wp-admin bar bell icon that show the WP-Notify sidebar - * - * @member {HTMLElement} wpNotifyHub - the Notification Hub Controller - * @event enableNotifyDrawer - When the user clicks or focus on the notification drawer, the drawer is enabled - * @event disableNotifyDrawer - on focus out - */ - wpNotifyHub.addEventListener( 'click', enableNotifyDrawer ); - wpNotifyHub.addEventListener( 'focus', enableNotifyDrawer, true ); - wpNotifyHub.addEventListener( 'blur', disableNotifyDrawer, true ); -} diff --git a/src/scripts/utils/sanitization.js b/src/scripts/utils/sanitization.js deleted file mode 100644 index abea8ebc..00000000 --- a/src/scripts/utils/sanitization.js +++ /dev/null @@ -1,13 +0,0 @@ -import DOMPurify from 'dompurify'; - -/** - * It takes a string and returns a sanitized version of that string. - * - * @param {string} string - The text to be purified. - * @param {Object} options - https://github.com/cure53/DOMPurify#can-i-configure-dompurify - */ -export const purify = ( string, options = {} ) => { - return { - __html: DOMPurify.sanitize( string, options ), - }; -}; From 4c796bb99d8cbfc3300793f0fb1f4fc2036e3cd0 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 8 Apr 2023 12:50:42 +0200 Subject: [PATCH 16/52] js utils functions updated --- src/scripts/utils/{effects.js => index.js} | 15 +++++++++- src/scripts/utils/init.js | 33 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) rename src/scripts/utils/{effects.js => index.js} (78%) create mode 100644 src/scripts/utils/init.js diff --git a/src/scripts/utils/effects.js b/src/scripts/utils/index.js similarity index 78% rename from src/scripts/utils/effects.js rename to src/scripts/utils/index.js index c231e091..66c0cd82 100644 --- a/src/scripts/utils/effects.js +++ b/src/scripts/utils/index.js @@ -1,6 +1,7 @@ +import { NOTIFY_NAMESPACE } from '../store/constants'; import { WEEK_IN_SECONDS } from '../components/NoticesArea'; import { dispatch } from '@wordpress/data'; -import { NOTIFY_NAMESPACE } from '../store/constants'; +import DOMPurify from 'dompurify'; /** * @typedef {import('../store').Notice} Notice @@ -45,3 +46,15 @@ export const getSorted = ( notifications, by = 'date' ) => { export const clearNotifyDrawer = ( context ) => { dispatch( NOTIFY_NAMESPACE ).clear( context ); }; + +/** + * It takes a string and returns a sanitized version of that string. + * + * @param {string} string - The text to be purified. + * @param {Object} options - https://github.com/cure53/DOMPurify#can-i-configure-dompurify + */ +export const purify = ( string, options = {} ) => { + return { + __html: DOMPurify.sanitize( string, options ), + }; +}; diff --git a/src/scripts/utils/init.js b/src/scripts/utils/init.js new file mode 100644 index 00000000..7d6734cf --- /dev/null +++ b/src/scripts/utils/init.js @@ -0,0 +1,33 @@ +import { createRoot } from '@wordpress/element'; +import { NoticesArea } from '../components/NoticesArea'; +import Hub from '../components/NotificationHub'; + +export function addContext( context ) { + /** Get the component container */ + const notifyContainer = document.getElementById( + `wp-notification-${ context }` + ); + + /** Creates a root for NoticesArea component. */ + const notifyRoot = createRoot( notifyContainer ); + + /** + * Renders the component into the specified context + * + * @member {HTMLElement} notifyDash - the area that will host the notifications + */ + notifyRoot.render( ); +} + +export function addHub() { + /** Get the Notification Hub area (admin bar) */ + const adminBarWpNotify = document.getElementById( + 'wp-admin-bar-wp-notification-hub' + ); + + /** Creates a root for Notification Hub area */ + const hubRoot = createRoot( adminBarWpNotify ); + + /** Init the Notification Hub component */ + hubRoot.render( ); +} From 30fec31cf66dde0c033bfae08af81334725af41a Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 8 Apr 2023 12:54:42 +0200 Subject: [PATCH 17/52] Notice component updated --- src/scripts/components/Notice.js | 2 +- src/scripts/components/NoticeEmpty.js | 5 ++++- src/scripts/components/NoticeHubSectionHeader.js | 2 +- src/scripts/components/NoticeImage.js | 2 +- src/scripts/components/NoticesArea.js | 4 ++-- src/scripts/store/constants.js | 8 ++++---- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/scripts/components/Notice.js b/src/scripts/components/Notice.js index 5a854847..0f27b05f 100644 --- a/src/scripts/components/Notice.js +++ b/src/scripts/components/Notice.js @@ -10,7 +10,7 @@ import { NoticeActions } from './NoticeAction'; // Import utilities import classnames from 'classnames'; -import { purify } from '../utils/sanitization'; +import { purify } from '../utils/'; import moment from 'moment'; import { defaultContext, NOTIFY_NAMESPACE } from '../store/constants'; import { dispatch } from '@wordpress/data'; diff --git a/src/scripts/components/NoticeEmpty.js b/src/scripts/components/NoticeEmpty.js index 68725dca..b7f3b569 100644 --- a/src/scripts/components/NoticeEmpty.js +++ b/src/scripts/components/NoticeEmpty.js @@ -2,7 +2,10 @@ import { comment, Icon } from '@wordpress/icons'; export const NoticeEmpty = ( props ) => { return ( -
+

{ props.message }

diff --git a/src/scripts/components/NoticeHubSectionHeader.js b/src/scripts/components/NoticeHubSectionHeader.js index 72267d5c..4b20f950 100644 --- a/src/scripts/components/NoticeHubSectionHeader.js +++ b/src/scripts/components/NoticeHubSectionHeader.js @@ -1,7 +1,7 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { check } from '@wordpress/icons'; -import { clearNotifyDrawer } from '../utils/effects'; +import { clearNotifyDrawer } from '../utils/'; /** * The section header for the notices section drawer. diff --git a/src/scripts/components/NoticeImage.js b/src/scripts/components/NoticeImage.js index ed0ad391..8cb4de79 100644 --- a/src/scripts/components/NoticeImage.js +++ b/src/scripts/components/NoticeImage.js @@ -1,7 +1,7 @@ import wpLogo from '../../images/WordPressLogo.svg'; import classNames from 'classnames'; import { defaultContext } from '../store/constants'; -import { purify } from '../utils/sanitization'; +import { purify } from '../utils/'; /** * It returns a div with a class name of `wp-notification-image` and `wp-notification-` plus the type of image passed in diff --git a/src/scripts/components/NoticesArea.js b/src/scripts/components/NoticesArea.js index 2d858b73..d32dd9b0 100644 --- a/src/scripts/components/NoticesArea.js +++ b/src/scripts/components/NoticesArea.js @@ -5,13 +5,13 @@ import { defaultContext, NOTIFY_NAMESPACE } from '../store/constants'; import { NoticeEmpty } from './NoticeEmpty'; import { NoticeHubSectionHeader } from './NoticeHubSectionHeader'; import { NoticesLoop } from './NoticesLoop'; -import { getSorted } from '../utils/effects'; +import { getSorted } from '../utils/'; import { NoticeHubFooter } from './NoticeHubFooter'; export const WEEK_IN_SECONDS = 1000 - 3600 * 24 * 7; /** - * WP-Notify toolbar in the secondary position of the admin bar + * WP Notification Feature toolbar in the secondary position of the admin bar * It watches for state updates and renders a component with the updated state * * @param {Object} props diff --git a/src/scripts/store/constants.js b/src/scripts/store/constants.js index 82bc2331..0ee5407e 100644 --- a/src/scripts/store/constants.js +++ b/src/scripts/store/constants.js @@ -1,20 +1,20 @@ /** - * @member {string} NOTIFY_NAMESPACE WP-Notify namespace + * @member {string} NOTIFY_NAMESPACE WP Notification Feature namespace */ export const NOTIFY_NAMESPACE = 'core/wp-notify'; /** - * @member {string} API_PATH WP-Notify rest api path + * @member {string} API_PATH WP Notification Feature rest api path */ export const API_PATH = '/wp/v2/notifications/'; /** - * @member {string} defaultContext WP-Notify default context + * @member {string} defaultContext WP Notification Feature default context */ export const defaultContext = 'adminbar'; /** - * @member {Object} context WP-Notify default contexts + * @member {Object} context WP Notification Feature default contexts */ export const contexts = [ defaultContext, 'dashboard' ]; From e352ffcb1ae78e8233002eacbb39adcb0d24cafc Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 8 Apr 2023 13:05:02 +0200 Subject: [PATCH 18/52] updated style to match the new components --- src/styles/notify/dashboard/notice.scss | 6 +- src/styles/notify/hub/admin-bar.scss | 87 ++++++++++++++----------- src/styles/notify/hub/elements.scss | 2 +- src/styles/notify/hub/layout.scss | 12 ++-- src/styles/notify/hub/notice.scss | 2 +- src/styles/notify/vars.scss | 4 +- src/styles/wp-notify.scss | 2 +- 7 files changed, 63 insertions(+), 52 deletions(-) diff --git a/src/styles/notify/dashboard/notice.scss b/src/styles/notify/dashboard/notice.scss index 387388ff..6fdc4a57 100644 --- a/src/styles/notify/dashboard/notice.scss +++ b/src/styles/notify/dashboard/notice.scss @@ -4,8 +4,7 @@ #wpbody { - #wp-notify-dashboard-notices, - #wp-notify-notice-demo { + #wp-notification-dashboard-notices { clear: both; } @@ -33,9 +32,6 @@ border-left-style: solid; border-color: var(--wp-notify_color-light-gray); - .dashicons { - } - // removes the top and the bottom margin from the first and the last notification of the dashboard &-wrap { > :first-child { diff --git a/src/styles/notify/hub/admin-bar.scss b/src/styles/notify/hub/admin-bar.scss index b0cb0475..9ea898a0 100644 --- a/src/styles/notify/hub/admin-bar.scss +++ b/src/styles/notify/hub/admin-bar.scss @@ -1,51 +1,64 @@ /* * WP admin bar */ -#wpadminbar #wp-admin-bar-wp-notify { +#wpadminbar #wp-admin-bar-wp-notification-hub { padding-right: 8px; position: relative; z-index: 100002; - /* this below is the bell located in the adminbar */ - > .ab-item .ab-icon:before { - content: "\f16d"; - top: 3px; - } + .notifications { - /* When the drawer is enabled transforms the bell icon to overlay the drawer */ - &.active > .ab-item .ab-icon { - filter: invert(1); - position: relative; - z-index: 100002; - } + /* the bell icon */ + .ab-item { + appearance: none; + height: 32px; + display: block; + padding: 0 10px; + border: 0; + margin: 0; + background: none; - /* The label is the red dot over the bell */ - .ab-label { - position: absolute; - width: 6px; - height: 6px; - top: 12px; - left: 20px; - border-radius: 50%; - font-size: 0; - - /* WP Editor / Alert Red */ - background: var(--wp-notify_color-red); - - /* Classic / Dark Gray 800 */ - border: 1px solid var(--wp-notify_color-light-gray-800); - } + .ab-icon:before { + content: "\f16d"; + top: 1px; + } + } + + /* When the drawer is enabled transforms the bell icon to overlay the drawer */ + &.active > .ab-item .ab-icon { + filter: invert(1); + position: relative; + z-index: 100002; + } - /* Mobile */ - @media #{$breakpoint} { - display: block; + /* The label is the red dot over the bell */ .ab-label { - clip: unset; - clip-path: unset; - width: 11px; - height: 11px; - top: 16px; - left: 28px; + position: absolute; + width: 6px; + height: 6px; + top: 12px; + left: 20px; + border-radius: 50%; + font-size: 0; + + /* WP Editor / Alert Red */ + background: var(--wp-notify_color-red); + + /* Classic / Dark Gray 800 */ + border: 1px solid var(--wp-notify_color-light-gray-800); + } + + /* Mobile */ + @media #{$breakpoint} { + display: block; + .ab-label { + clip: unset; + clip-path: unset; + width: 11px; + height: 11px; + top: 16px; + left: 28px; + } } } } diff --git a/src/styles/notify/hub/elements.scss b/src/styles/notify/hub/elements.scss index f173253f..87ea5021 100644 --- a/src/styles/notify/hub/elements.scss +++ b/src/styles/notify/hub/elements.scss @@ -1,7 +1,7 @@ /* * Hub Elements style (header, footer etc) */ -#wp-notification-hub { +#notification-hub { * { margin: 0; padding: 0; diff --git a/src/styles/notify/hub/layout.scss b/src/styles/notify/hub/layout.scss index e1b7083b..9a6ef60b 100644 --- a/src/styles/notify/hub/layout.scss +++ b/src/styles/notify/hub/layout.scss @@ -1,7 +1,7 @@ /* * Notification Hub Layout */ -#wp-notification-hub { +#notification-hub { position: fixed; right: calc(0px - var(--wp-notify_hub-width)); top: 0; @@ -19,13 +19,13 @@ opacity: 0; transition: transform 350ms, opacity 250ms; - #wp-admin-bar-wp-notify.active & { + .notifications.active & { opacity: 1; transform: translateX(-100%); padding-bottom: 0; // moves the shadow at the end of sidebar } - // reset child icons color since by default the icon is lightened when the mouse is in hover state + // Reset child icons color since by default the icon is lightened when the mouse is in hover state #wpadminbar &:hover, #wpadminbar &:active { .wp-notification-image .ab-icon:before { @@ -33,7 +33,7 @@ } } - .wp-notification-hub-wrapper { + .hub-wrapper { position: relative; height: inherit; overflow: auto; @@ -50,9 +50,11 @@ box-shadow: 0 10px 24px 2px var(--wp-notify_color-default); width: 80%; margin: auto; + position: absolute; + bottom: 0; } - // scrollbar customization + // Scrollbar customization &::-webkit-scrollbar { width: 18px; // width = scrollbar width + border * 2 height: 48px; diff --git a/src/styles/notify/hub/notice.scss b/src/styles/notify/hub/notice.scss index 80ef1795..a51af6c7 100644 --- a/src/styles/notify/hub/notice.scss +++ b/src/styles/notify/hub/notice.scss @@ -1,7 +1,7 @@ /* * The single notification */ -#wp-notification-hub { +#notification-hub { .wp-notification { max-width: calc(var(--wp-notify_hub-width) - var(--wp-notify_hub-spacing-gap) * 2); diff --git a/src/styles/notify/vars.scss b/src/styles/notify/vars.scss index 20510a0d..78471e2a 100644 --- a/src/styles/notify/vars.scss +++ b/src/styles/notify/vars.scss @@ -1,6 +1,6 @@ // https://make.wordpress.org/design/handbook/design-guide/foundations/colors/ :root { - // WP-Notify base colors + // WP Notification Feature base colors --wp-notify_color-default: #404040; --wp-notify_color-white: #fff; // Dark Gray 300 --wp-notify_color-light-gray-400: #E8EAEB; // Light Gray 400 @@ -20,7 +20,7 @@ --wp-notify_color-warning: #FFB900; --wp-notify_color-success: #46B450; - // WP-Notify hub (admin top bar) + // WP Notification Feature hub (admin top bar) --wp-notify_hub-width: 320px; --wp-notify_hub-image-size: 32px; diff --git a/src/styles/wp-notify.scss b/src/styles/wp-notify.scss index 8f48e404..718b2eea 100644 --- a/src/styles/wp-notify.scss +++ b/src/styles/wp-notify.scss @@ -1,5 +1,5 @@ /* - * WP-Notify Style + * WP Notification Feature Style */ // Vars From 245033a57465cd90abd39916fa87d9aca18eb310 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 8 Apr 2023 13:21:39 +0200 Subject: [PATCH 19/52] init updated --- includes/demo.php | 17 +++-------------- src/scripts/wp-notify.js | 36 ++---------------------------------- 2 files changed, 5 insertions(+), 48 deletions(-) diff --git a/includes/demo.php b/includes/demo.php index 9de24668..5b643fbd 100644 --- a/includes/demo.php +++ b/includes/demo.php @@ -15,22 +15,11 @@ function wp_admin_bar_wp_notify_item( WP_Admin_Bar $wp_admin_bar ) { return; } - $aside = sprintf( - '', - __( 'Notifications' ) - ); - $args = array( - 'id' => 'wp-notify', - 'title' => sprintf( "%s", __( 'Notifications' ) ), + 'id' => 'wp-notification-hub', + 'title' => __( 'loading' ), 'parent' => 'top-secondary', 'meta' => array( - 'html' => $aside, 'tabindex' => 0, ), ); @@ -42,7 +31,7 @@ function wp_admin_bar_wp_notify_item( WP_Admin_Bar $wp_admin_bar ) { * Adds WP Notify area at the top of the dashboard */ function wp_notify_admin_notice() { - echo '
'; + echo '
'; } add_action( 'admin_notices', 'wp_notify_admin_notice' ); diff --git a/src/scripts/wp-notify.js b/src/scripts/wp-notify.js index ce4c6639..fe492b00 100644 --- a/src/scripts/wp-notify.js +++ b/src/scripts/wp-notify.js @@ -1,13 +1,9 @@ /** WordPress Dependencies */ -import { createRoot } from '@wordpress/element'; import { dispatch, select } from '@wordpress/data'; -/** WP Notify - Components */ -import { NoticesArea } from './components/NoticesArea'; -import Drawer from './components/Drawer'; - /** The store default data */ import { NOTIFY_NAMESPACE, contexts } from './store/constants'; +import { addContext, addHub } from './utils/init'; /** * @typedef {import('./store').Notice} Notice @@ -87,39 +83,11 @@ contexts.forEach( ( context ) => /** after registering contexts we could fetch the notifications */ select( NOTIFY_NAMESPACE ).fetchUpdates(); -function addContext( context ) { - /** Get the component container */ - const notifyContainer = document.getElementById( `wp-notify-${ context }` ); - - /** Creates a root for NoticesArea component. */ - const notifyRoot = createRoot( notifyContainer ); - - /** - * Renders the component into the specified context - * - * @member {HTMLElement} notifyDash - the area that will host the notifications - */ - notifyRoot.render( ); -} - -function addDrawer() { - /** Get the Notification Hub area (admin bar) */ - const adminBarWpNotify = document.getElementById( - 'wp-admin-bar-wp-notify' - ); - - /** Creates a root for Notification Hub area */ - const hubRoot = createRoot( adminBarWpNotify ); - - /** Init the Notification Hub component */ - hubRoot.render( ); -} - /** * Loops into contexts and adds a NoticesArea component for each one */ contexts.forEach( ( context ) => - context === 'adminbar' ? addDrawer() : addContext( context ) + context === 'adminbar' ? addHub() : addContext( context ) ); /** From 7d9321d1aa07c0e85bbea92be8cf81646fe5f39e Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 8 Apr 2023 14:46:43 +0200 Subject: [PATCH 20/52] solving merge conflicts // duplicated code --- src/scripts/components/Notice.js | 2 +- src/scripts/components/NoticesArea.js | 5 ++++- src/scripts/utils/index.js | 13 ------------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/scripts/components/Notice.js b/src/scripts/components/Notice.js index b415afc8..c6a78827 100644 --- a/src/scripts/components/Notice.js +++ b/src/scripts/components/Notice.js @@ -10,7 +10,7 @@ import { NoticeActions } from './NoticeAction'; // Import utilities import classnames from 'classnames'; -import { purify } from '../utils/'; +import { purify } from '../utils/sanitization'; import moment from 'moment'; import { defaultContext, NOTIFY_NAMESPACE } from '../store/constants'; import { dispatch } from '@wordpress/data'; diff --git a/src/scripts/components/NoticesArea.js b/src/scripts/components/NoticesArea.js index f26ff521..9938bcf9 100644 --- a/src/scripts/components/NoticesArea.js +++ b/src/scripts/components/NoticesArea.js @@ -37,7 +37,10 @@ export const NoticesArea = ( props ) => { /* * Todo: this method should supply to rest api the user data, current page, moreover the request args may be added (notice per page, notice filters and sort) */ - notifications = useSelect( store, [] ).getNotices( context ); + notifications = useSelect( + ( select ) => select( store, [] ).getNotices( context ), + [] + ); /** * if the context is the adminbar we need to render a list of notifications with the recent notifications and the old notifications diff --git a/src/scripts/utils/index.js b/src/scripts/utils/index.js index 66c0cd82..bba1fd1f 100644 --- a/src/scripts/utils/index.js +++ b/src/scripts/utils/index.js @@ -1,7 +1,6 @@ import { NOTIFY_NAMESPACE } from '../store/constants'; import { WEEK_IN_SECONDS } from '../components/NoticesArea'; import { dispatch } from '@wordpress/data'; -import DOMPurify from 'dompurify'; /** * @typedef {import('../store').Notice} Notice @@ -46,15 +45,3 @@ export const getSorted = ( notifications, by = 'date' ) => { export const clearNotifyDrawer = ( context ) => { dispatch( NOTIFY_NAMESPACE ).clear( context ); }; - -/** - * It takes a string and returns a sanitized version of that string. - * - * @param {string} string - The text to be purified. - * @param {Object} options - https://github.com/cure53/DOMPurify#can-i-configure-dompurify - */ -export const purify = ( string, options = {} ) => { - return { - __html: DOMPurify.sanitize( string, options ), - }; -}; From 864a7b17f275818d2497bb4228e05ea0429b4ebe Mon Sep 17 00:00:00 2001 From: John Hooks Date: Sat, 8 Apr 2023 06:04:10 -0700 Subject: [PATCH 21/52] fix: remove second dashboard message --- includes/restapi/fake_api.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/includes/restapi/fake_api.json b/includes/restapi/fake_api.json index 62fcefa3..271e2e46 100644 --- a/includes/restapi/fake_api.json +++ b/includes/restapi/fake_api.json @@ -15,23 +15,6 @@ "acceptLink": "https://github.com/WordPress/wp-feature-notifications" } }, - { - "id": 2, - "context": "dashboard", - "title": "Message variant #1", - "date": 1654866071, - "message": "This is an example of on-page message variant #1. It has a title, a message, an image, an action button with a URL, is dismissable.", - "source": "#Test", - "dismissible": true, - "unread": true, - "icon": { - "src": "https://source.unsplash.com/random/400×400/?notify" - }, - "action": { - "acceptMessage": "TEST", - "acceptLink": "https://github.com/WordPress/wp-notify" - } - }, { "id": 3, "title": "Message variant #1 (copy)", From b9b94a7545c83d22edc03cd8d49dd1ee994c1333 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 8 Apr 2023 15:33:39 +0200 Subject: [PATCH 22/52] enables again focus/blur to show the drawer with keyboard --- src/scripts/components/Drawer.js | 4 ++-- src/scripts/components/NotificationHub.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/scripts/components/Drawer.js b/src/scripts/components/Drawer.js index 20e0ce49..c5926424 100644 --- a/src/scripts/components/Drawer.js +++ b/src/scripts/components/Drawer.js @@ -1,9 +1,9 @@ import { __ } from '@wordpress/i18n'; import { NoticesArea } from './NoticesArea'; -export default () => { +export default ( { focus, blur } ) => { return ( -