From c58243452dcc2499155b64248c5cbaff402746dd Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 7 Apr 2023 15:49:01 -0700 Subject: [PATCH 1/2] 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 2/2] 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;