Migrate @woocommerce/notice to TS

This commit is contained in:
Chi-Hsuan Huang 2022-05-20 13:09:25 +08:00
parent 2885a0ae86
commit 286358d724
11 changed files with 122 additions and 50 deletions

View File

@ -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,14 @@
"lint:fix": "eslint src --fix"
},
"devDependencies": {
"@automattic/data-stores": "^2.0.1",
"@babel/core": "^7.17.5",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/data": "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"

View File

@ -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,30 +49,43 @@ 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 = {} ) {
const {
speak = true,
isDismissible = true,
context = DEFAULT_CONTEXT,
id = uniqueId( context ),
actions = [],
type = 'default',
export function createNotice(
status: Status = DEFAULT_STATUS,
content: string,
{
speak,
isDismissible,
context,
id,
actions,
type,
__unstableHTML,
icon = null,
explicitDismiss = false,
onDismiss = null,
} = options;
icon,
explicitDismiss,
onDismiss,
}: Options = {
speak: true,
isDismissible: true,
context: DEFAULT_CONTEXT,
id: uniqueId( DEFAULT_CONTEXT ),
actions: [],
type: 'default',
icon: null,
explicitDismiss: false,
onDismiss: null,
}
) {
// 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 +114,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 +129,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 +144,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 +159,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 +172,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 >;

View File

@ -12,4 +12,4 @@ export const DEFAULT_CONTEXT = 'global';
*
* @type {string}
*/
export const DEFAULT_STATUS = 'info';
export const DEFAULT_STATUS = 'info' as const;

View File

@ -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' );
},
};

View File

@ -2,6 +2,8 @@
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores';
import { Reducer, AnyAction } from 'redux';
/**
* Internal dependencies
@ -9,11 +11,24 @@ import { registerStore } from '@wordpress/data';
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( 'core/notices2', {
reducer,
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 >;
}

View File

@ -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 );

View File

@ -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;
}

View File

@ -0,0 +1,10 @@
/**
* Internal dependencies
*/
import { createNotice } from './actions';
export type Notices = Array< ReturnType< typeof createNotice >[ 'notice' ] >;
export type State = {
[ context: string ]: Notices;
};

View File

@ -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 ];

View File

@ -2,6 +2,9 @@
"extends": "../tsconfig",
"compilerOptions": {
"rootDir": "src",
"outDir": "build-module"
"outDir": "build-module",
"declaration": true,
"declarationMap": true,
"declarationDir": "./build-types"
}
}
}