diff --git a/packages/js/notices/changelog/dev-33098-migrate-woo-notice-to-ts b/packages/js/notices/changelog/dev-33098-migrate-woo-notice-to-ts new file mode 100644 index 00000000000..13cf6e2b7b2 --- /dev/null +++ b/packages/js/notices/changelog/dev-33098-migrate-woo-notice-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate @woocommerce/notices to TS diff --git a/packages/js/notices/package.json b/packages/js/notices/package.json index 927b41d98ee..fd88c0a4790 100644 --- a/packages/js/notices/package.json +++ b/packages/js/notices/package.json @@ -20,6 +20,7 @@ }, "main": "build/index.js", "module": "build-module/index.js", + "types": "build-types", "react-native": "src/index", "dependencies": { "@wordpress/a11y": "^3.5.0", @@ -45,11 +46,13 @@ "lint:fix": "eslint src --fix" }, "devDependencies": { + "@automattic/data-stores": "^2.0.1", "@babel/core": "^7.17.5", "@woocommerce/eslint-plugin": "workspace:*", "eslint": "^8.12.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", + "redux": "^4.2.0", "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" diff --git a/packages/js/notices/src/index.js b/packages/js/notices/src/index.ts similarity index 100% rename from packages/js/notices/src/index.js rename to packages/js/notices/src/index.ts diff --git a/packages/js/notices/src/store/actions.js b/packages/js/notices/src/store/actions.ts similarity index 81% rename from packages/js/notices/src/store/actions.js rename to packages/js/notices/src/store/actions.ts index 59fa585e0ef..0bc43dbe076 100644 --- a/packages/js/notices/src/store/actions.js +++ b/packages/js/notices/src/store/actions.ts @@ -2,22 +2,25 @@ * External dependencies */ import { uniqueId } from 'lodash'; +import { Status, Action as WPNoticeAction } from '@wordpress/notices'; /** * Internal dependencies */ import { DEFAULT_CONTEXT, DEFAULT_STATUS } from './constants'; -/** - * @typedef {Object} WPNoticeAction Object describing a user action option associated with a notice. - * - * @property {string} label Message to use as action label. - * @property {?string} url Optional URL of resource if action incurs - * browser navigation. - * @property {?Function} onClick Optional function to invoke when action is - * triggered by user. - * - */ +export type Options = { + id: string; + context: string; + isDismissible: boolean; + type: string; + speak: boolean; + actions: Array< WPNoticeAction >; + icon: null | JSX.Element; + explicitDismiss: boolean; + onDismiss: ( () => void ) | null; + __unstableHTML?: boolean; +}; /** * Returns an action object used in signalling that a notice is to be created. @@ -46,10 +49,15 @@ import { DEFAULT_CONTEXT, DEFAULT_STATUS } from './constants'; * can't be dismissed by clicking * the body of the notice. * @param {Function} [options.onDismiss] Called when the notice is dismissed. + * @param {boolean} [options.__unstableHTML] Notice message as raw HTML. * - * @return {Object} Action object. + * @return {Object} WPNoticeAction object. */ -export function createNotice( status = DEFAULT_STATUS, content, options = {} ) { +export function createNotice( + status: Status = DEFAULT_STATUS, + content: string, + options: Partial< Options > = {} +) { const { speak = true, isDismissible = true, @@ -62,14 +70,13 @@ export function createNotice( status = DEFAULT_STATUS, content, options = {} ) { explicitDismiss = false, onDismiss = null, } = options; - // The supported value shape of content is currently limited to plain text // strings. To avoid setting expectation that e.g. a WPElement could be // supported, cast to a string. content = String( content ); return { - type: 'CREATE_NOTICE', + type: 'CREATE_NOTICE' as const, context, notice: { id, @@ -98,7 +105,7 @@ export function createNotice( status = DEFAULT_STATUS, content, options = {} ) { * * @return {Object} Action object. */ -export function createSuccessNotice( content, options ) { +export function createSuccessNotice( content: string, options: Options ) { return createNotice( 'success', content, options ); } @@ -113,7 +120,7 @@ export function createSuccessNotice( content, options ) { * * @return {Object} Action object. */ -export function createInfoNotice( content, options ) { +export function createInfoNotice( content: string, options: Options ) { return createNotice( 'info', content, options ); } @@ -128,7 +135,7 @@ export function createInfoNotice( content, options ) { * * @return {Object} Action object. */ -export function createErrorNotice( content, options ) { +export function createErrorNotice( content: string, options: Options ) { return createNotice( 'error', content, options ); } @@ -143,7 +150,7 @@ export function createErrorNotice( content, options ) { * * @return {Object} Action object. */ -export function createWarningNotice( content, options ) { +export function createWarningNotice( content: string, options: Options ) { return createNotice( 'warning', content, options ); } @@ -156,10 +163,12 @@ export function createWarningNotice( content, options ) { * * @return {Object} Action object. */ -export function removeNotice( id, context = DEFAULT_CONTEXT ) { +export function removeNotice( id: string, context: string = DEFAULT_CONTEXT ) { return { - type: 'REMOVE_NOTICE', + type: 'REMOVE_NOTICE' as const, id, context, }; } + +export type Action = ReturnType< typeof createNotice | typeof removeNotice >; diff --git a/packages/js/notices/src/store/constants.js b/packages/js/notices/src/store/constants.ts similarity index 88% rename from packages/js/notices/src/store/constants.js rename to packages/js/notices/src/store/constants.ts index 2949bde0577..f0a3fbcb622 100644 --- a/packages/js/notices/src/store/constants.js +++ b/packages/js/notices/src/store/constants.ts @@ -12,4 +12,4 @@ export const DEFAULT_CONTEXT = 'global'; * * @type {string} */ -export const DEFAULT_STATUS = 'info'; +export const DEFAULT_STATUS = 'info' as const; diff --git a/packages/js/notices/src/store/controls.js b/packages/js/notices/src/store/controls.ts similarity index 63% rename from packages/js/notices/src/store/controls.js rename to packages/js/notices/src/store/controls.ts index 704352289d5..6da646a78e7 100644 --- a/packages/js/notices/src/store/controls.js +++ b/packages/js/notices/src/store/controls.ts @@ -3,8 +3,13 @@ */ import { speak } from '@wordpress/a11y'; +export type Action = { + message: string; + ariaLive?: string; +}; + export default { - SPEAK( action ) { + SPEAK( action: Action ) { speak( action.message, action.ariaLive || 'assertive' ); }, }; diff --git a/packages/js/notices/src/store/index.js b/packages/js/notices/src/store/index.js deleted file mode 100644 index d4ebaff9a6a..00000000000 --- a/packages/js/notices/src/store/index.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * External dependencies - */ -import { registerStore } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import reducer from './reducer'; -import * as actions from './actions'; -import * as selectors from './selectors'; - -// NOTE: This uses core/notices2, if this file is copied back upstream -// to Gutenberg this needs to be changed back to core/notices. -export default registerStore( 'core/notices2', { - reducer, - actions, - selectors, -} ); diff --git a/packages/js/notices/src/store/index.ts b/packages/js/notices/src/store/index.ts new file mode 100644 index 00000000000..64cf25b27de --- /dev/null +++ b/packages/js/notices/src/store/index.ts @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; +import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores'; +import { Reducer, AnyAction } from 'redux'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as actions from './actions'; +import * as selectors from './selectors'; +import { State } from './types'; +export * from './types'; + +export const STORE_NAME = 'core/notices2'; +// NOTE: This uses core/notices2, if this file is copied back upstream +// to Gutenberg this needs to be changed back to core/notices. +export default registerStore< State >( STORE_NAME, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + selectors, +} ); + +declare module '@wordpress/data' { + // TODO: convert action.js to TS + function dispatch( + key: typeof STORE_NAME + ): DispatchFromMap< typeof actions >; + function select( + key: typeof STORE_NAME + ): SelectFromMap< typeof selectors >; +} diff --git a/packages/js/notices/src/store/reducer.js b/packages/js/notices/src/store/reducer.ts similarity index 54% rename from packages/js/notices/src/store/reducer.js rename to packages/js/notices/src/store/reducer.ts index ca68877d8d0..0fff8197b94 100644 --- a/packages/js/notices/src/store/reducer.js +++ b/packages/js/notices/src/store/reducer.ts @@ -2,22 +2,24 @@ * External dependencies */ import { reject } from 'lodash'; +import type { Reducer } from 'redux'; /** * Internal dependencies */ import onSubKey from './utils/on-sub-key'; +import { Action } from './actions'; +import { Notices } from './types'; /** - * 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. + * Reducer returning the next notices state. The notices state is an array of notice objects * - * @param {Object} state Current state. + * @param {Array} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ -const notices = onSubKey( 'context' )( ( state = [], action ) => { +const notices: Reducer< Notices, Action > = ( state = [], action ) => { switch ( action.type ) { case 'CREATE_NOTICE': // Avoid duplicates on ID. @@ -29,8 +31,12 @@ const notices = onSubKey( 'context' )( ( state = [], action ) => { case 'REMOVE_NOTICE': return reject( state, { id: action.id } ); } - return state; -} ); +}; -export default notices; +export type State = { + [ context: string ]: Notices; +}; + +// Creates a combined reducer object where each key is a context, its value an array of notice objects. +export default onSubKey( 'context' )( notices ); diff --git a/packages/js/notices/src/store/selectors.js b/packages/js/notices/src/store/selectors.ts similarity index 94% rename from packages/js/notices/src/store/selectors.js rename to packages/js/notices/src/store/selectors.ts index d03d8812d53..8daed4e3c9f 100644 --- a/packages/js/notices/src/store/selectors.js +++ b/packages/js/notices/src/store/selectors.ts @@ -2,6 +2,7 @@ * Internal dependencies */ import { DEFAULT_CONTEXT } from './constants'; +import { State } from './reducer'; /** @typedef {import('./actions').WPNoticeAction} WPNoticeAction */ @@ -14,7 +15,7 @@ import { DEFAULT_CONTEXT } from './constants'; * * @type {Array} */ -const DEFAULT_NOTICES = []; +const DEFAULT_NOTICES: [ ] = []; /** * @typedef {Object} WPNotice Notice object. @@ -51,6 +52,6 @@ const DEFAULT_NOTICES = []; * * @return {WPNotice[]} Array of notices. */ -export function getNotices( state, context = DEFAULT_CONTEXT ) { +export function getNotices( state: State, context: string = DEFAULT_CONTEXT ) { return state[ context ] || DEFAULT_NOTICES; } diff --git a/packages/js/notices/src/store/types.ts b/packages/js/notices/src/store/types.ts new file mode 100644 index 00000000000..69048b58061 --- /dev/null +++ b/packages/js/notices/src/store/types.ts @@ -0,0 +1,10 @@ +/** + * Internal dependencies + */ +import { createNotice } from './actions'; + +export type Notices = Array< ReturnType< typeof createNotice >[ 'notice' ] >; + +export type State = { + [ context: string ]: Notices; +}; diff --git a/packages/js/notices/src/store/utils/on-sub-key.js b/packages/js/notices/src/store/utils/on-sub-key.ts similarity index 70% rename from packages/js/notices/src/store/utils/on-sub-key.js rename to packages/js/notices/src/store/utils/on-sub-key.ts index 24adf06b773..cb29221c7b5 100644 --- a/packages/js/notices/src/store/utils/on-sub-key.js +++ b/packages/js/notices/src/store/utils/on-sub-key.ts @@ -1,3 +1,14 @@ +/** + * External dependencies + */ +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import { State, Notices } from '../types'; +import { Action } from '../actions'; + /** * Higher-order reducer creator which creates a combined reducer object, keyed * by a property on the action object. @@ -6,10 +17,9 @@ * * @return {Function} Higher-order reducer. */ -export const onSubKey = ( actionProperty ) => ( reducer ) => ( - state = {}, - action -) => { +export const onSubKey = ( actionProperty: keyof Action ) => ( + reducer: Reducer< Notices, Action > +) => ( state: State = {}, action: Action ) => { // Retrieve subkey from action. Do not track if undefined; useful for cases // where reducer is scoped by action shape. const key = action[ actionProperty ]; diff --git a/packages/js/notices/tsconfig.json b/packages/js/notices/tsconfig.json index e8f14a25fa4..ea9f201d401 100644 --- a/packages/js/notices/tsconfig.json +++ b/packages/js/notices/tsconfig.json @@ -2,6 +2,9 @@ "extends": "../tsconfig", "compilerOptions": { "rootDir": "src", - "outDir": "build-module" + "outDir": "build-module", + "declaration": true, + "declarationMap": true, + "declarationDir": "./build-types" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23c55654f09..3ba4796639b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1011,6 +1011,7 @@ importers: packages/js/notices: specifiers: + '@automattic/data-stores': ^2.0.1 '@babel/core': ^7.17.5 '@woocommerce/eslint-plugin': workspace:* '@wordpress/a11y': ^3.5.0 @@ -1019,6 +1020,7 @@ importers: eslint: ^8.12.0 jest: ^27.5.1 jest-cli: ^27.5.1 + redux: ^4.2.0 rimraf: ^3.0.2 ts-jest: ^27.1.3 typescript: ^4.6.2 @@ -1027,11 +1029,13 @@ importers: '@wordpress/data': 6.4.1 '@wordpress/notices': 3.4.1 devDependencies: + '@automattic/data-stores': 2.0.1_@wordpress+data@6.4.1 '@babel/core': 7.17.8 '@woocommerce/eslint-plugin': link:../eslint-plugin eslint: 8.12.0 jest: 27.5.1 jest-cli: 27.5.1 + redux: 4.2.0 rimraf: 3.0.2 ts-jest: 27.1.3_b64eba0a9f1c7068f7f3a75addda5ccd typescript: 4.6.2 @@ -34661,6 +34665,12 @@ packages: dependencies: '@babel/runtime': 7.17.7 + /redux/4.2.0: + resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} + dependencies: + '@babel/runtime': 7.17.7 + dev: true + /reflect.ownkeys/0.2.0: resolution: {integrity: sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=}