From 02b47a67dbc16b3d691ee36c607bba05366424d3 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 17 Aug 2022 14:32:38 +0800 Subject: [PATCH] Migrate @woocommerce/data navigation store to TS (#34239) * Migrate woo data navigation store to TS * Add changelog * Fix action type * Update packages/js/data/src/navigation/reducer.ts Co-authored-by: Ilyas Foo * Add a doc comment to MenuItem type Co-authored-by: Ilyas Foo --- ...-33839-migrate-data-navigation-store-to-ts | 4 ++ .../{action-types.js => action-types.ts} | 2 +- .../src/navigation/{actions.js => actions.ts} | 58 ++++++++++++------- packages/js/data/src/navigation/constants.ts | 2 +- .../{dispatchers.js => dispatchers.ts} | 0 packages/js/data/src/navigation/index.js | 26 --------- packages/js/data/src/navigation/index.ts | 38 ++++++++++++ .../src/navigation/{reducer.js => reducer.ts} | 36 +++++++----- .../navigation/{resolvers.js => resolvers.ts} | 2 +- .../navigation/{selectors.js => selectors.ts} | 18 ++++-- .../test/{reducer.js => reducer.ts} | 20 +++++++ packages/js/data/src/navigation/types.ts | 30 ++++++++++ ...ation.js => with-navigation-hydration.tsx} | 9 ++- 13 files changed, 174 insertions(+), 71 deletions(-) create mode 100644 packages/js/data/changelog/dev-33839-migrate-data-navigation-store-to-ts rename packages/js/data/src/navigation/{action-types.js => action-types.ts} (98%) rename packages/js/data/src/navigation/{actions.js => actions.ts} (59%) rename packages/js/data/src/navigation/{dispatchers.js => dispatchers.ts} (100%) delete mode 100644 packages/js/data/src/navigation/index.js create mode 100644 packages/js/data/src/navigation/index.ts rename packages/js/data/src/navigation/{reducer.js => reducer.ts} (75%) rename packages/js/data/src/navigation/{resolvers.js => resolvers.ts} (92%) rename packages/js/data/src/navigation/{selectors.js => selectors.ts} (50%) rename packages/js/data/src/navigation/test/{reducer.js => reducer.ts} (81%) create mode 100644 packages/js/data/src/navigation/types.ts rename packages/js/data/src/navigation/{with-navigation-hydration.js => with-navigation-hydration.tsx} (71%) diff --git a/packages/js/data/changelog/dev-33839-migrate-data-navigation-store-to-ts b/packages/js/data/changelog/dev-33839-migrate-data-navigation-store-to-ts new file mode 100644 index 00000000000..340c4ad89ff --- /dev/null +++ b/packages/js/data/changelog/dev-33839-migrate-data-navigation-store-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate navigation store to TS diff --git a/packages/js/data/src/navigation/action-types.js b/packages/js/data/src/navigation/action-types.ts similarity index 98% rename from packages/js/data/src/navigation/action-types.js rename to packages/js/data/src/navigation/action-types.ts index 2735b2e892d..af2fe6a3a6b 100644 --- a/packages/js/data/src/navigation/action-types.js +++ b/packages/js/data/src/navigation/action-types.ts @@ -11,6 +11,6 @@ const TYPES = { REMOVE_FAVORITE_FAILURE: 'REMOVE_FAVORITE_FAILURE', REMOVE_FAVORITE_REQUEST: 'REMOVE_FAVORITE_REQUEST', REMOVE_FAVORITE_SUCCESS: 'REMOVE_FAVORITE_SUCCESS', -}; +} as const; export default TYPES; diff --git a/packages/js/data/src/navigation/actions.js b/packages/js/data/src/navigation/actions.ts similarity index 59% rename from packages/js/data/src/navigation/actions.js rename to packages/js/data/src/navigation/actions.ts index a4c748c65db..f073046b203 100644 --- a/packages/js/data/src/navigation/actions.js +++ b/packages/js/data/src/navigation/actions.ts @@ -9,50 +9,51 @@ import { getPersistedQuery } from '@woocommerce/navigation'; */ import TYPES from './action-types'; import { WC_ADMIN_NAMESPACE } from '../constants'; +import { MenuItem } from './types'; -export function setMenuItems( menuItems ) { +export function setMenuItems( menuItems: MenuItem[] ) { return { type: TYPES.SET_MENU_ITEMS, menuItems, }; } -export function addMenuItems( menuItems ) { +export function addMenuItems( menuItems: MenuItem[] ) { return { type: TYPES.ADD_MENU_ITEMS, menuItems, }; } -export function getFavoritesFailure( error ) { +export function getFavoritesFailure( error: unknown ) { return { type: TYPES.GET_FAVORITES_FAILURE, error, }; } -export function getFavoritesRequest( favorites ) { +export function getFavoritesRequest( favorites?: string[] ) { return { type: TYPES.GET_FAVORITES_REQUEST, favorites, }; } -export function getFavoritesSuccess( favorites ) { +export function getFavoritesSuccess( favorites: string[] ) { return { type: TYPES.GET_FAVORITES_SUCCESS, favorites, }; } -export function addFavoriteRequest( favorite ) { +export function addFavoriteRequest( favorite: string ) { return { type: TYPES.ADD_FAVORITE_REQUEST, favorite, }; } -export function addFavoriteFailure( favorite, error ) { +export function addFavoriteFailure( favorite: string, error: unknown ) { return { type: TYPES.ADD_FAVORITE_FAILURE, favorite, @@ -60,21 +61,21 @@ export function addFavoriteFailure( favorite, error ) { }; } -export function addFavoriteSuccess( favorite ) { +export function addFavoriteSuccess( favorite: string ) { return { type: TYPES.ADD_FAVORITE_SUCCESS, favorite, }; } -export function removeFavoriteRequest( favorite ) { +export function removeFavoriteRequest( favorite: string ) { return { type: TYPES.REMOVE_FAVORITE_REQUEST, favorite, }; } -export function removeFavoriteFailure( favorite, error ) { +export function removeFavoriteFailure( favorite: string, error: unknown ) { return { type: TYPES.REMOVE_FAVORITE_FAILURE, favorite, @@ -82,18 +83,13 @@ export function removeFavoriteFailure( favorite, error ) { }; } -export function removeFavoriteSuccess( favorite, error ) { +export function removeFavoriteSuccess( favorite: string ) { return { type: TYPES.REMOVE_FAVORITE_SUCCESS, favorite, - error, }; } -export function* onLoad() { - yield onHistoryChange(); -} - export function* onHistoryChange() { const persistedQuery = getPersistedQuery(); @@ -107,11 +103,15 @@ export function* onHistoryChange() { }; } -export function* addFavorite( favorite ) { +export function* onLoad() { + yield onHistoryChange(); +} + +export function* addFavorite( favorite: string ) { yield addFavoriteRequest( favorite ); try { - const results = yield apiFetch( { + const results: string[] = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/navigation/favorites/me`, method: 'POST', data: { @@ -131,11 +131,11 @@ export function* addFavorite( favorite ) { } } -export function* removeFavorite( favorite ) { +export function* removeFavorite( favorite: string ) { yield removeFavoriteRequest( favorite ); try { - const results = yield apiFetch( { + const results: string[] = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/navigation/favorites/me`, method: 'DELETE', data: { @@ -154,3 +154,21 @@ export function* removeFavorite( favorite ) { throw new Error(); } } + +export type Action = ReturnType< + | typeof setMenuItems + | typeof addMenuItems + | typeof getFavoritesFailure + | typeof getFavoritesRequest + | typeof getFavoritesSuccess + | typeof addFavoriteRequest + | typeof addFavoriteFailure + | typeof addFavoriteSuccess + | typeof removeFavoriteRequest + | typeof removeFavoriteFailure + | typeof removeFavoriteSuccess + | ( () => { + type: typeof TYPES.ON_HISTORY_CHANGE; + persistedQuery: ReturnType< typeof getPersistedQuery >; + } ) +>; diff --git a/packages/js/data/src/navigation/constants.ts b/packages/js/data/src/navigation/constants.ts index cc7f6a71b7f..702ad57d0d5 100644 --- a/packages/js/data/src/navigation/constants.ts +++ b/packages/js/data/src/navigation/constants.ts @@ -1 +1 @@ -export const STORE_NAME = 'woocommerce-navigation'; +export const STORE_NAME = 'woocommerce-navigation' as const; diff --git a/packages/js/data/src/navigation/dispatchers.js b/packages/js/data/src/navigation/dispatchers.ts similarity index 100% rename from packages/js/data/src/navigation/dispatchers.js rename to packages/js/data/src/navigation/dispatchers.ts diff --git a/packages/js/data/src/navigation/index.js b/packages/js/data/src/navigation/index.js deleted file mode 100644 index c18c238591c..00000000000 --- a/packages/js/data/src/navigation/index.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * External dependencies - */ -import { controls } from '@wordpress/data-controls'; -import { registerStore } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from './constants'; -import * as selectors from './selectors'; -import * as actions from './actions'; -import reducer from './reducer'; -import * as resolvers from './resolvers'; -import initDispatchers from './dispatchers'; - -registerStore( STORE_NAME, { - reducer, - actions, - controls, - resolvers, - selectors, -} ); - -initDispatchers(); -export const NAVIGATION_STORE_NAME = STORE_NAME; diff --git a/packages/js/data/src/navigation/index.ts b/packages/js/data/src/navigation/index.ts new file mode 100644 index 00000000000..7e3b8155a0a --- /dev/null +++ b/packages/js/data/src/navigation/index.ts @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; +import { controls } from '@wordpress/data-controls'; +import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores'; +import { Reducer, AnyAction } from 'redux'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from './constants'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import reducer, { State } from './reducer'; +import * as resolvers from './resolvers'; +import initDispatchers from './dispatchers'; +import { WPDataActions, WPDataSelectors } from '../types'; + +registerStore< State >( STORE_NAME, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + controls, + resolvers, + selectors, +} ); + +initDispatchers(); +export const NAVIGATION_STORE_NAME = STORE_NAME; + +declare module '@wordpress/data' { + function dispatch( + key: typeof STORE_NAME + ): DispatchFromMap< typeof actions & WPDataActions >; + function select( + key: typeof STORE_NAME + ): SelectFromMap< typeof selectors > & WPDataSelectors; +} diff --git a/packages/js/data/src/navigation/reducer.js b/packages/js/data/src/navigation/reducer.ts similarity index 75% rename from packages/js/data/src/navigation/reducer.js rename to packages/js/data/src/navigation/reducer.ts index 39b7f46009e..0b051f35591 100644 --- a/packages/js/data/src/navigation/reducer.js +++ b/packages/js/data/src/navigation/reducer.ts @@ -1,9 +1,16 @@ +/** + * External dependencies + */ +import type { Reducer } from 'redux'; + /** * Internal dependencies */ import TYPES from './action-types'; +import { Action } from './actions'; +import { NavigationState } from './types'; -const reducer = ( +const reducer: Reducer< NavigationState, Action > = ( state = { error: null, menuItems: [], @@ -11,25 +18,25 @@ const reducer = ( requesting: {}, persistedQuery: {}, }, - { type, error, favorite, favorites, menuItems, persistedQuery } + action ) => { - switch ( type ) { + switch ( action.type ) { case TYPES.SET_MENU_ITEMS: state = { ...state, - menuItems, + menuItems: action.menuItems, }; break; case TYPES.ADD_MENU_ITEMS: state = { ...state, - menuItems: [ ...state.menuItems, ...menuItems ], + menuItems: [ ...state.menuItems, ...action.menuItems ], }; break; case TYPES.ON_HISTORY_CHANGE: state = { ...state, - persistedQuery, + persistedQuery: action.persistedQuery, }; break; case TYPES.GET_FAVORITES_FAILURE: @@ -53,7 +60,7 @@ const reducer = ( case TYPES.GET_FAVORITES_SUCCESS: state = { ...state, - favorites, + favorites: action.favorites, requesting: { ...state.requesting, getFavorites: false, @@ -63,7 +70,7 @@ const reducer = ( case TYPES.ADD_FAVORITE_FAILURE: state = { ...state, - error, + error: action.error, requesting: { ...state.requesting, addFavorite: false, @@ -80,15 +87,15 @@ const reducer = ( }; break; case TYPES.ADD_FAVORITE_SUCCESS: - const newFavorites = ! state.favorites.includes( favorite ) - ? [ ...state.favorites, favorite ] + const newFavorites = ! state.favorites.includes( action.favorite ) + ? [ ...state.favorites, action.favorite ] : state.favorites; state = { ...state, favorites: newFavorites, menuItems: state.menuItems.map( ( item ) => { - if ( item.id === favorite ) { + if ( item.id === action.favorite ) { return { ...item, menuId: 'favorites', @@ -107,7 +114,7 @@ const reducer = ( ...state, requesting: { ...state.requesting, - error, + error: action.error, removeFavorite: false, }, }; @@ -123,14 +130,14 @@ const reducer = ( break; case TYPES.REMOVE_FAVORITE_SUCCESS: const filteredFavorites = state.favorites.filter( - ( f ) => f !== favorite + ( f ) => f !== action.favorite ); state = { ...state, favorites: filteredFavorites, menuItems: state.menuItems.map( ( item ) => { - if ( item.id === favorite ) { + if ( item.id === action.favorite ) { return { ...item, menuId: 'plugins', @@ -148,4 +155,5 @@ const reducer = ( return state; }; +export type State = ReturnType< typeof reducer >; export default reducer; diff --git a/packages/js/data/src/navigation/resolvers.js b/packages/js/data/src/navigation/resolvers.ts similarity index 92% rename from packages/js/data/src/navigation/resolvers.js rename to packages/js/data/src/navigation/resolvers.ts index 0f87a184290..88697b9bab0 100644 --- a/packages/js/data/src/navigation/resolvers.js +++ b/packages/js/data/src/navigation/resolvers.ts @@ -17,7 +17,7 @@ export function* getFavorites() { yield getFavoritesRequest(); try { - const results = yield apiFetch( { + const results: string[] = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/navigation/favorites/me`, } ); diff --git a/packages/js/data/src/navigation/selectors.js b/packages/js/data/src/navigation/selectors.ts similarity index 50% rename from packages/js/data/src/navigation/selectors.js rename to packages/js/data/src/navigation/selectors.ts index 1579631eb97..6436a8da32a 100644 --- a/packages/js/data/src/navigation/selectors.js +++ b/packages/js/data/src/navigation/selectors.ts @@ -3,9 +3,14 @@ */ import { applyFilters } from '@wordpress/hooks'; -const MENU_ITEMS_HOOK = 'woocommerce_navigation_menu_items'; +/** + * Internal dependencies + */ +import { NavigationState } from './types'; -export const getMenuItems = ( state ) => { +const MENU_ITEMS_HOOK = 'woocommerce_navigation_menu_items' as const; + +export const getMenuItems = ( state: NavigationState ) => { /** * Navigation Menu Items. * @@ -15,14 +20,17 @@ export const getMenuItems = ( state ) => { return applyFilters( MENU_ITEMS_HOOK, state.menuItems ); }; -export const getFavorites = ( state ) => { +export const getFavorites = ( state: NavigationState ) => { return state.favorites || []; }; -export const isNavigationRequesting = ( state, selector ) => { +export const isNavigationRequesting = ( + state: NavigationState, + selector: string +) => { return state.requesting[ selector ] || false; }; -export const getPersistedQuery = ( state ) => { +export const getPersistedQuery = ( state: NavigationState ) => { return state.persistedQuery || {}; }; diff --git a/packages/js/data/src/navigation/test/reducer.js b/packages/js/data/src/navigation/test/reducer.ts similarity index 81% rename from packages/js/data/src/navigation/test/reducer.js rename to packages/js/data/src/navigation/test/reducer.ts index ccb6fbe6cbb..28be120cdcb 100644 --- a/packages/js/data/src/navigation/test/reducer.js +++ b/packages/js/data/src/navigation/test/reducer.ts @@ -14,6 +14,7 @@ const defaultState = { describe( 'navigation reducer', () => { it( 'should return a default state', () => { + // @ts-expect-error -- we're testing the reducer's default state. const state = reducer( undefined, {} ); expect( state ).toEqual( defaultState ); expect( state ).not.toBe( defaultState ); @@ -27,16 +28,25 @@ describe( 'navigation reducer', () => { id: 'menu-item-1', title: 'Menu Item 1', menuId: 'primary', + url: 'https://example.com/menu-item-1', + migrate: true, + order: 1, }, { id: 'menu-item-2', title: 'Menu Item 2', menuId: 'primary', + url: 'https://example.com/menu-item-2', + migrate: true, + order: 2, }, { id: 'menu-item-3', title: 'Menu Item 3', menuId: 'secondary', + url: 'https://example.com/menu-item-3', + migrate: true, + order: 3, }, ], } ); @@ -55,8 +65,15 @@ describe( 'navigation reducer', () => { id: 'menu-item-1', title: 'Menu Item 1', menuId: 'primary', + url: 'https://example.com/menu-item-1', + migrate: true, + order: 1, }, ], + error: null, + favorites: [], + requesting: {}, + persistedQuery: {}, }, { type: TYPES.ADD_MENU_ITEMS, @@ -65,6 +82,9 @@ describe( 'navigation reducer', () => { id: 'menu-item-2', title: 'Menu Item 2', menuId: 'primary', + url: 'https://example.com/menu-item-2', + migrate: true, + order: 2, }, ], } diff --git a/packages/js/data/src/navigation/types.ts b/packages/js/data/src/navigation/types.ts new file mode 100644 index 00000000000..03e06b1d25f --- /dev/null +++ b/packages/js/data/src/navigation/types.ts @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { getPersistedQuery } from '@woocommerce/navigation'; + +// https://github.com/woocommerce/woocommerce/blob/ecec9eaa76cb7b2e36f79175837b71f4f64996b1/plugins/woocommerce/src/Admin/Features/Navigation/Menu.php +export type MenuItem = { + id: string; + title: string; + url: string; + order: number; + migrate: boolean; + menuId: string; + isCategory?: boolean; + badge?: number; + backButtonLabel?: string; + parent?: string; + capability?: string; + matchExpression?: string; +}; + +export type NavigationState = { + error: null | unknown; + menuItems: MenuItem[]; + favorites: string[]; + requesting: { + [ key: string ]: boolean | unknown; + }; + persistedQuery: ReturnType< typeof getPersistedQuery >; +}; diff --git a/packages/js/data/src/navigation/with-navigation-hydration.js b/packages/js/data/src/navigation/with-navigation-hydration.tsx similarity index 71% rename from packages/js/data/src/navigation/with-navigation-hydration.js rename to packages/js/data/src/navigation/with-navigation-hydration.tsx index 4bc56a1080e..e212bf3b88f 100644 --- a/packages/js/data/src/navigation/with-navigation-hydration.js +++ b/packages/js/data/src/navigation/with-navigation-hydration.tsx @@ -9,17 +9,20 @@ import { createElement, useRef } from '@wordpress/element'; * Internal dependencies */ import { STORE_NAME } from './constants'; +import { MenuItem } from './types'; /** * Higher-order component used to hydrate navigation data. * - * @param {Object} data Data object with menu items and site information. + * @param {Object} data Data object with menu items and site information. + * @param {MenuItem[]} data.menuItems Menu items to hydrate. */ -export const withNavigationHydration = ( data ) => - createHigherOrderComponent( +export const withNavigationHydration = ( data: { menuItems: MenuItem[] } ) => + createHigherOrderComponent< Record< string, unknown > >( ( OriginalComponent ) => ( props ) => { const dataRef = useRef( data ); + // @ts-expect-error // @ts-expect-error registry is not defined in the wp.data typings useSelect( ( select, registry ) => { if ( ! dataRef.current ) { return;