diff --git a/packages/js/data/changelog/dev-migrate-item-store-to-ts b/packages/js/data/changelog/dev-migrate-item-store-to-ts new file mode 100644 index 00000000000..b607b39bdb6 --- /dev/null +++ b/packages/js/data/changelog/dev-migrate-item-store-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate item store to TS diff --git a/packages/js/data/src/items/action-types.js b/packages/js/data/src/items/action-types.ts similarity index 93% rename from packages/js/data/src/items/action-types.js rename to packages/js/data/src/items/action-types.ts index 4bea03c6d13..6fc29e87923 100644 --- a/packages/js/data/src/items/action-types.js +++ b/packages/js/data/src/items/action-types.ts @@ -3,6 +3,6 @@ const TYPES = { SET_ITEMS: 'SET_ITEMS', SET_ITEMS_TOTAL_COUNT: 'SET_ITEMS_TOTAL_COUNT', SET_ERROR: 'SET_ERROR', -}; +} as const; export default TYPES; diff --git a/packages/js/data/src/items/actions.js b/packages/js/data/src/items/actions.ts similarity index 63% rename from packages/js/data/src/items/actions.js rename to packages/js/data/src/items/actions.ts index 71662a367f9..e4e54bc91c1 100644 --- a/packages/js/data/src/items/actions.js +++ b/packages/js/data/src/items/actions.ts @@ -9,8 +9,9 @@ import { addQueryArgs } from '@wordpress/url'; */ import TYPES from './action-types'; import { NAMESPACE, WC_ADMIN_NAMESPACE } from '../constants'; +import { ItemType, Item, ProductItem, Query, ItemID } from './types'; -export function setItem( itemType, id, item ) { +export function setItem( itemType: ItemType, id: ItemID, item: Item ) { return { type: TYPES.SET_ITEM, id, @@ -19,7 +20,12 @@ export function setItem( itemType, id, item ) { }; } -export function setItems( itemType, query, items, totalCount ) { +export function setItems( + itemType: ItemType, + query: Query, + items: Item[], + totalCount?: number +) { return { type: TYPES.SET_ITEMS, items, @@ -29,7 +35,11 @@ export function setItems( itemType, query, items, totalCount ) { }; } -export function setItemsTotalCount( itemType, query, totalCount ) { +export function setItemsTotalCount( + itemType: ItemType, + query: Query, + totalCount: number +) { return { type: TYPES.SET_ITEMS_TOTAL_COUNT, itemType, @@ -38,7 +48,11 @@ export function setItemsTotalCount( itemType, query, totalCount ) { }; } -export function setError( itemType, query, error ) { +export function setError( + itemType: ItemType | 'createProductFromTemplate', + query: Record< string, unknown >, + error: unknown +) { return { type: TYPES.SET_ERROR, itemType, @@ -47,7 +61,10 @@ export function setError( itemType, query, error ) { }; } -export function* updateProductStock( product, quantity ) { +export function* updateProductStock( + product: Partial< ProductItem > & { id: ProductItem[ 'id' ] }, + quantity: number +) { const updatedProduct = { ...product, stock_quantity: quantity }; const { id, parent_id: parentId, type } = updatedProduct; @@ -75,18 +92,24 @@ export function* updateProductStock( product, quantity ) { } catch ( error ) { // Update failed, return product back to original state. yield setItem( 'products', id, product ); - yield setError( 'products', id, error ); + yield setError( 'products', { id }, error ); return false; } } -export function* createProductFromTemplate( itemFields, query ) { +export function* createProductFromTemplate( + itemFields: { + template_name: string; + status: string; + }, + query: Query +) { try { const url = addQueryArgs( `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/create_product_from_template`, query || {} ); - const newItem = yield apiFetch( { + const newItem: { id: ProductItem[ 'id' ] } = yield apiFetch( { path: url, method: 'POST', data: itemFields, @@ -98,3 +121,10 @@ export function* createProductFromTemplate( itemFields, query ) { throw error; } } + +export type Action = ReturnType< + | typeof setItem + | typeof setItems + | typeof setItemsTotalCount + | typeof setError +>; diff --git a/packages/js/data/src/items/constants.ts b/packages/js/data/src/items/constants.ts index dbe32f31d2e..baf7809bf80 100644 --- a/packages/js/data/src/items/constants.ts +++ b/packages/js/data/src/items/constants.ts @@ -1 +1 @@ -export const STORE_NAME = 'wc/admin/items'; +export const STORE_NAME = 'wc/admin/items' as const; diff --git a/packages/js/data/src/items/index.js b/packages/js/data/src/items/index.js deleted file mode 100644 index 7b3ca7e73fa..00000000000 --- a/packages/js/data/src/items/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * External dependencies - */ - -import { registerStore } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from './constants'; -import * as selectors from './selectors'; -import * as actions from './actions'; -import * as resolvers from './resolvers'; -import controls from '../controls'; -import reducer from './reducer'; - -registerStore( STORE_NAME, { - reducer, - actions, - controls, - selectors, - resolvers, -} ); - -export const ITEMS_STORE_NAME = STORE_NAME; diff --git a/packages/js/data/src/items/index.ts b/packages/js/data/src/items/index.ts new file mode 100644 index 00000000000..bd6cb4938ae --- /dev/null +++ b/packages/js/data/src/items/index.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; +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 * as resolvers from './resolvers'; +import reducer, { State } from './reducer'; +import controls from '../controls'; +import { WPDataActions, WPDataSelectors } from '../types'; +import { getItemsType } from './selectors'; +export * from './types'; +export type { State }; + +registerStore< State >( STORE_NAME, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + controls, + selectors, + resolvers, +} ); + +export const ITEMS_STORE_NAME = STORE_NAME; + +export type ItemsSelector = Omit< + SelectFromMap< typeof selectors >, + 'getItems' +> & { + getItems: getItemsType; +} & WPDataSelectors; + +declare module '@wordpress/data' { + function dispatch( + key: typeof STORE_NAME + ): DispatchFromMap< typeof actions & WPDataActions >; + function select( key: typeof STORE_NAME ): ItemsSelector; +} diff --git a/packages/js/data/src/items/reducer.js b/packages/js/data/src/items/reducer.js deleted file mode 100644 index 60707994250..00000000000 --- a/packages/js/data/src/items/reducer.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Internal dependencies - */ -import TYPES from './action-types'; -import { getResourceName } from '../utils'; -import { getTotalCountResourceName } from './utils'; - -const reducer = ( - state = { - items: {}, - errors: {}, - data: {}, - }, - { type, id, itemType, query, item, items, totalCount, error } -) => { - switch ( type ) { - case TYPES.SET_ITEM: - const itemData = state.data[ itemType ] || {}; - return { - ...state, - data: { - ...state.data, - [ itemType ]: { - ...itemData, - [ id ]: { - ...( itemData[ id ] || {} ), - ...item, - }, - }, - }, - }; - case TYPES.SET_ITEMS: - const ids = []; - const nextItems = items.reduce( ( result, theItem ) => { - ids.push( theItem.id ); - result[ theItem.id ] = theItem; - return result; - }, {} ); - const resourceName = getResourceName( itemType, query ); - return { - ...state, - items: { - ...state.items, - [ resourceName ]: { data: ids }, - }, - data: { - ...state.data, - [ itemType ]: { - ...state.data[ itemType ], - ...nextItems, - }, - }, - }; - case TYPES.SET_ITEMS_TOTAL_COUNT: - const totalResourceName = getTotalCountResourceName( - itemType, - query - ); - return { - ...state, - items: { - ...state.items, - [ totalResourceName ]: totalCount, - }, - }; - case TYPES.SET_ERROR: - return { - ...state, - errors: { - ...state.errors, - [ getResourceName( itemType, query ) ]: error, - }, - }; - default: - return state; - } -}; - -export default reducer; diff --git a/packages/js/data/src/items/reducer.ts b/packages/js/data/src/items/reducer.ts new file mode 100644 index 00000000000..b50e9b3f2de --- /dev/null +++ b/packages/js/data/src/items/reducer.ts @@ -0,0 +1,97 @@ +/** + * External dependencies + */ + +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import TYPES from './action-types'; +import { getResourceName } from '../utils'; +import { getTotalCountResourceName } from './utils'; +import { Action } from './actions'; +import { ItemsState, Item, ItemID } from './types'; + +const initialState: ItemsState = { + items: {}, + errors: {}, + data: {}, +}; + +const reducer: Reducer< ItemsState, Action > = ( + state = initialState, + action +) => { + switch ( action.type ) { + case TYPES.SET_ITEM: + const itemData = state.data[ action.itemType ] || {}; + return { + ...state, + data: { + ...state.data, + [ action.itemType ]: { + ...itemData, + [ action.id ]: { + ...( itemData[ action.id ] || {} ), + ...action.item, + }, + }, + }, + }; + case TYPES.SET_ITEMS: + const ids: Array< ItemID > = []; + const nextItems = action.items.reduce< Record< ItemID, Item > >( + ( result, theItem ) => { + ids.push( theItem.id ); + result[ theItem.id ] = theItem; + return result; + }, + {} + ); + const resourceName = getResourceName( + action.itemType, + action.query + ); + return { + ...state, + items: { + ...state.items, + [ resourceName ]: { data: ids }, + }, + data: { + ...state.data, + [ action.itemType ]: { + ...state.data[ action.itemType ], + ...nextItems, + }, + }, + }; + case TYPES.SET_ITEMS_TOTAL_COUNT: + const totalResourceName = getTotalCountResourceName( + action.itemType, + action.query + ); + return { + ...state, + items: { + ...state.items, + [ totalResourceName ]: action.totalCount, + }, + }; + case TYPES.SET_ERROR: + return { + ...state, + errors: { + ...state.errors, + [ getResourceName( action.itemType, action.query ) ]: + action.error, + }, + }; + default: + return state; + } +}; + +export type State = ReturnType< typeof reducer >; +export default reducer; diff --git a/packages/js/data/src/items/resolvers.js b/packages/js/data/src/items/resolvers.ts similarity index 79% rename from packages/js/data/src/items/resolvers.js rename to packages/js/data/src/items/resolvers.ts index 9f5fbc99084..390995b9b0c 100644 --- a/packages/js/data/src/items/resolvers.js +++ b/packages/js/data/src/items/resolvers.ts @@ -4,8 +4,9 @@ import { NAMESPACE } from '../constants'; import { setError, setItems, setItemsTotalCount } from './actions'; import { request } from '../utils'; +import { ItemType, Query } from './types'; -export function* getItems( itemType, query ) { +export function* getItems( itemType: ItemType, query: Query ) { try { const endpoint = itemType === 'categories' ? 'products/categories' : itemType; @@ -13,6 +14,7 @@ export function* getItems( itemType, query ) { `${ NAMESPACE }/${ endpoint }`, query ); + yield setItemsTotalCount( itemType, query, totalCount ); yield setItems( itemType, query, items ); } catch ( error ) { @@ -20,11 +22,7 @@ export function* getItems( itemType, query ) { } } -export function* getReviewsTotalCount( itemType, query ) { - yield getItemsTotalCount( itemType, query ); -} - -export function* getItemsTotalCount( itemType, query ) { +export function* getItemsTotalCount( itemType: ItemType, query: Query ) { try { const totalsQuery = { ...query, @@ -42,3 +40,7 @@ export function* getItemsTotalCount( itemType, query ) { yield setError( itemType, query, error ); } } + +export function* getReviewsTotalCount( itemType: ItemType, query: Query ) { + yield getItemsTotalCount( itemType, query ); +} diff --git a/packages/js/data/src/items/selectors.js b/packages/js/data/src/items/selectors.js deleted file mode 100644 index 5e019fe4334..00000000000 --- a/packages/js/data/src/items/selectors.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; - -/** - * Internal dependencies - */ -import { getResourceName } from '../utils'; -import { getTotalCountResourceName } from './utils'; - -export const getItems = createSelector( - ( state, itemType, query, defaultValue = new Map() ) => { - const resourceName = getResourceName( itemType, query ); - const ids = - state.items[ resourceName ] && state.items[ resourceName ].data; - if ( ! ids ) { - return defaultValue; - } - return ids.reduce( ( map, id ) => { - map.set( id, state.data[ itemType ][ id ] ); - return map; - }, new Map() ); - }, - ( state, itemType, query ) => { - const resourceName = getResourceName( itemType, query ); - return [ state.items[ resourceName ] ]; - } -); - -export const getItemsTotalCount = ( - state, - itemType, - query, - defaultValue = 0 -) => { - const resourceName = getTotalCountResourceName( itemType, query ); - const totalCount = state.items.hasOwnProperty( resourceName ) - ? state.items[ resourceName ] - : defaultValue; - return totalCount; -}; - -export const getItemsError = ( state, itemType, query ) => { - const resourceName = getResourceName( itemType, query ); - return state.errors[ resourceName ]; -}; diff --git a/packages/js/data/src/items/selectors.ts b/packages/js/data/src/items/selectors.ts new file mode 100644 index 00000000000..9285827129a --- /dev/null +++ b/packages/js/data/src/items/selectors.ts @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + +/** + * Internal dependencies + */ +import { getResourceName } from '../utils'; +import { getTotalCountResourceName } from './utils'; + +import { ItemType, ItemsState, Query, ItemInfer } from './types'; + +export type getItemsType = < T extends ItemType >( + itemType: T, + query: Query, + defaultValue?: Map< number, ItemInfer< T > | undefined > +) => Map< number, ItemInfer< T > | undefined >; + +type getItemsSelectorType = < T extends ItemType >( + state: ItemsState, + itemType: T, + query: Query, + defaultValue?: Map< number, ItemInfer< T > | undefined > +) => Map< number, Map< number, ItemInfer< T > | undefined > >; + +export const getItems = createSelector< getItemsSelectorType >( + ( state, itemType, query, defaultValue = new Map() ) => { + const resourceName = getResourceName( itemType, query ); + + let ids; + if ( + state.items[ resourceName ] && + typeof state.items[ resourceName ] === 'object' + ) { + ids = ( state.items[ resourceName ] as Record< string, number[] > ) + .data; + } + + if ( ! ids ) { + return defaultValue; + } + return ids.reduce( ( map, id: number ) => { + map.set( id, state.data[ itemType ]?.[ id ] ); + return map; + }, new Map() ); + }, + ( state, itemType, query ) => { + const resourceName = getResourceName( itemType, query ); + return [ state.items[ resourceName ] ]; + } +); + +export const getItemsTotalCount = ( + state: ItemsState, + itemType: ItemType, + query: Query, + defaultValue = 0 +) => { + const resourceName = getTotalCountResourceName( itemType, query ); + const totalCount = state.items.hasOwnProperty( resourceName ) + ? state.items[ resourceName ] + : defaultValue; + return totalCount; +}; + +export const getItemsError = ( + state: ItemsState, + itemType: ItemType, + query: Query +) => { + const resourceName = getResourceName( itemType, query ); + return state.errors[ resourceName ]; +}; diff --git a/packages/js/data/src/items/test/reducer.js b/packages/js/data/src/items/test/reducer.ts similarity index 62% rename from packages/js/data/src/items/test/reducer.js rename to packages/js/data/src/items/test/reducer.ts index e7c73e40677..5b299c6989e 100644 --- a/packages/js/data/src/items/test/reducer.js +++ b/packages/js/data/src/items/test/reducer.ts @@ -5,6 +5,7 @@ import reducer from '../reducer'; import TYPES from '../action-types'; import { getResourceName } from '../../utils'; import { getTotalCountResourceName } from '../utils'; +import { ProductItem } from '../types'; const defaultState = { items: {}, @@ -14,13 +15,14 @@ const defaultState = { describe( 'items reducer', () => { it( 'should return a default state', () => { + // @ts-expect-error - we're testing the default state const state = reducer( undefined, {} ); expect( state ).toEqual( defaultState ); expect( state ).not.toBe( defaultState ); } ); it( 'should handle SET_ITEM', () => { - const itemType = 'guyisms'; + const itemType = 'products'; const initialState = { items: { [ itemType ]: { @@ -31,14 +33,14 @@ describe( 'items reducer', () => { errors: {}, data: { [ itemType ]: { - 1: { id: 1, title: 'Donkey', status: 'flavortown' }, - 2: { id: 2, title: 'Sauce', status: 'flavortown' }, + 1: { id: 1, name: 'Donkey', tax_status: 'flavortown' }, + 2: { id: 2, name: 'Sauce', tax_status: 'flavortown' }, }, }, }; const update = { id: 2, - status: 'bomb dot com', + tax_status: 'bomb dot com', }; const state = reducer( initialState, { @@ -51,26 +53,28 @@ describe( 'items reducer', () => { expect( state.items ).toEqual( initialState.items ); expect( state.errors ).toEqual( initialState.errors ); - expect( state.data[ itemType ][ '1' ] ).toEqual( - initialState.data[ itemType ][ '1' ] - ); - expect( state.data[ itemType ][ '2' ].id ).toEqual( + const item = state.data[ itemType ] || {}; + + expect( item[ '1' ] ).toEqual( initialState.data[ itemType ][ '1' ] ); + expect( item[ '2' ].id ).toEqual( initialState.data[ itemType ][ '2' ].id ); - expect( state.data[ itemType ][ '2' ].title ).toEqual( - initialState.data[ itemType ][ '2' ].title + expect( ( item[ '2' ] as ProductItem ).name ).toEqual( + initialState.data[ itemType ][ '2' ].name + ); + expect( ( item[ '2' ] as Partial< ProductItem > ).tax_status ).toEqual( + update.tax_status ); - expect( state.data[ itemType ][ '2' ].status ).toEqual( update.status ); } ); it( 'should handle SET_ITEMS', () => { const items = [ - { id: 1, title: 'Yum!' }, - { id: 2, title: 'Dynamite!' }, + { id: 1, name: 'Yum!' }, + { id: 2, name: 'Dynamite!' }, ]; const totalCount = 45; - const query = { status: 'flavortown' }; - const itemType = 'BBQ'; + const query = { page: 1 }; + const itemType = 'products'; const state = reducer( defaultState, { type: TYPES.SET_ITEMS, items, @@ -81,16 +85,31 @@ describe( 'items reducer', () => { const resourceName = getResourceName( itemType, query ); - expect( state.items[ resourceName ].data ).toHaveLength( 2 ); - expect( state.items[ resourceName ].data.includes( 1 ) ).toBeTruthy(); - expect( state.items[ resourceName ].data.includes( 2 ) ).toBeTruthy(); + expect( + ( state.items[ resourceName ] as { [ key: string ]: number[] } ) + .data + ).toHaveLength( 2 ); + expect( + ( + state.items[ resourceName ] as { + [ key: string ]: number[]; + } + ).data.includes( 1 ) + ).toBeTruthy(); + expect( + ( + state.items[ resourceName ] as { + [ key: string ]: number[]; + } + ).data.includes( 2 ) + ).toBeTruthy(); - expect( state.data[ itemType ][ '1' ] ).toBe( items[ 0 ] ); - expect( state.data[ itemType ][ '2' ] ).toBe( items[ 1 ] ); + expect( ( state.data[ itemType ] || {} )[ '1' ] ).toBe( items[ 0 ] ); + expect( ( state.data[ itemType ] || {} )[ '2' ] ).toBe( items[ 1 ] ); } ); it( 'should handle SET_ITEMS_TOTAL_COUNT', () => { - const itemType = 'BBQ'; + const itemType = 'products'; const initialQuery = { status: 'flavortown', page: 1, @@ -105,6 +124,8 @@ describe( 'items reducer', () => { items: { [ resourceName ]: 1, }, + data: {}, + errors: {}, }; // Additional coverage for getTotalCountResourceName(). @@ -112,7 +133,7 @@ describe( 'items reducer', () => { status: 'flavortown', page: 2, per_page: 10, - _fields: [ 'id', 'title', 'status' ], + _fields: [ 'id', 'name', 'status' ], }; const state = reducer( initialState, { @@ -123,6 +144,8 @@ describe( 'items reducer', () => { } ); expect( state ).toEqual( { + data: {}, + errors: {}, items: { [ resourceName ]: 2, }, @@ -131,7 +154,7 @@ describe( 'items reducer', () => { it( 'should handle SET_ERROR', () => { const query = { status: 'flavortown' }; - const itemType = 'BBQ'; + const itemType = 'products'; const resourceName = getResourceName( itemType, query ); const error = 'Baaam!'; const state = reducer( defaultState, { diff --git a/packages/js/data/src/items/test/utils.js b/packages/js/data/src/items/test/utils.ts similarity index 100% rename from packages/js/data/src/items/test/utils.js rename to packages/js/data/src/items/test/utils.ts diff --git a/packages/js/data/src/items/types.ts b/packages/js/data/src/items/types.ts new file mode 100644 index 00000000000..1e542cd3efd --- /dev/null +++ b/packages/js/data/src/items/types.ts @@ -0,0 +1,229 @@ +/** + * Internal dependencies + */ +import { BaseQueryParams } from '../types/query-params'; + +type Link = { + href: string; +}; + +// Category, Product, Customer item id is a number, and leaderboards item is a string. +export type ItemID = number | string; + +export type ItemType = 'categories' | 'products' | 'customers' | 'leaderboards'; + +export type ItemImage = { + id: number; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + src: string; + name: string; + alt: string; +}; + +// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php#L97-L208 +export type CategoryItem = { + id: number; + name: string; + slug: string; + parent: number; + description: string; + display: string; + image: null | ItemImage; + menu_order: number; + count: number; + _links: { + collection: Array< Link >; + self: Array< Link >; + }; +}; + +// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Products.php#L72-L83 +// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php#L809-L1423 +export type ProductItem = { + id: number; + name: string; + slug: string; + permalink: string; + attributes: Array< { + id: number; + name: string; + position: number; + visible: boolean; + variation: boolean; + options: string[]; + } >; + average_rating: string; + backordered: boolean; + backorders: string; + backorders_allowed: boolean; + button_text: string; + catalog_visibility: string; + categories: Array< { + id: number; + name: string; + slug: string; + } >; + cross_sell_ids: number[]; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + date_on_sale_from: null | string; + date_on_sale_from_gmt: null | string; + date_on_sale_to: null | string; + date_on_sale_to_gmt: null | string; + default_attributes: Array< { + id: number; + name: string; + option: string; + } >; + description: string; + dimensions: { length: string; width: string; height: string }; + download_expiry: number; + download_limit: number; + downloadable: boolean; + downloads: Array< { + id: number; + name: string; + file: string; + } >; + external_url: string; + featured: boolean; + grouped_products: Array< number >; + has_options: boolean; + images: Array< ItemImage >; + low_stock_amount: null | number; + manage_stock: boolean; + menu_order: number; + meta_data: Array< { + id: number; + key: string; + value: string; + } >; + on_sale: boolean; + parent_id: number; + price: string; + price_html: string; + purchasable: boolean; + purchase_note: string; + rating_count: number; + regular_price: string; + related_ids: number[]; + reviews_allowed: boolean; + sale_price: string; + shipping_class: string; + shipping_class_id: number; + shipping_required: boolean; + shipping_taxable: boolean; + short_description: string; + sku: string; + sold_individually: boolean; + status: string; + stock_quantity: number; + stock_status: string; + tags: Array< { + id: number; + name: string; + slug: string; + } >; + tax_class: string; + tax_status: string; + total_sales: number; + type: string; + upsell_ids: number[]; + variations: Array< { + id: number; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + attributes: Array< { + id: number; + name: string; + option: string; + } >; + image: string; + price: string; + regular_price: string; + sale_price: string; + sku: string; + stock_quantity: number; + tax_class: string; + tax_status: string; + total_sales: number; + weight: string; + } >; + virtual: boolean; + weight: string; + last_order_date: string; +}; + +// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php#L221-L318 +export type CustomerItem = { + id: number; + user_id: number; + name: string; + username: string; + country: string; + city: string; + state: string; + postcode: string; + date_registered: string; + date_registered_gmt: string; + date_last_active: string; + date_last_active_gmt: string; + orders_count: number; + total_spent: number; + avg_order_value: number; + _links: { + self: Array< Link >; + }; +}; + +// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Leaderboards.php#L527-L585 +export type LeaderboardItem = { + id: string; + label: string; + headers: { + label: string; + }; + rows: { + display: string; + value: string; + }; +}; + +export type Item = Partial< + CategoryItem | ProductItem | CustomerItem | LeaderboardItem +> & { + id: ItemID; +}; + +export type ItemInfer< T > = Partial< + T extends 'categories' + ? CategoryItem + : T extends 'products' + ? ProductItem + : T extends 'customers' + ? CustomerItem + : T extends 'leaderboards' + ? LeaderboardItem + : never +> & { + id: ItemID; +}; + +export type ItemsState = { + items: + | Record< string, { data: ItemID[] } | number > + | Record< string, never >; + data: Partial< Record< ItemType, Record< ItemID, Item > > >; + errors: { + [ key: string ]: unknown; + }; +}; + +export type Query = Partial< BaseQueryParams >; diff --git a/packages/js/data/src/items/utils.js b/packages/js/data/src/items/utils.ts similarity index 76% rename from packages/js/data/src/items/utils.js rename to packages/js/data/src/items/utils.ts index 085270c0624..2ac29d8647b 100644 --- a/packages/js/data/src/items/utils.js +++ b/packages/js/data/src/items/utils.ts @@ -2,12 +2,24 @@ * External dependencies */ import { appendTimestamp, getCurrentDates } from '@woocommerce/date'; - +import { select as wpSelect } from '@wordpress/data'; /** * Internal dependencies */ import { STORE_NAME } from './constants'; import { getResourceName } from '../utils'; +import { ItemInfer, ItemType, Query } from './types'; +import { ItemsSelector } from './'; + +type Options = { + id: number; + per_page: number; + persisted_query: Query; + filterQuery: Query; + query: { [ key: string ]: string | undefined }; + select: typeof wpSelect; + defaultDateRange: string; +}; /** * Returns leaderboard data to render a leaderboard table. @@ -17,11 +29,12 @@ import { getResourceName } from '../utils'; * @param {number} options.per_page Per page limit * @param {Object} options.persisted_query Persisted query passed to endpoint * @param {Object} options.query Query parameters in the url + * @param {Object} options.filterQuery Query parameters to filter the leaderboard * @param {Object} options.select Instance of @wordpress/select * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object containing leaderboard responses. */ -export function getLeaderboard( options ) { +export function getLeaderboard( options: Options ) { const endpoint = 'leaderboards'; const { per_page: perPage, @@ -30,6 +43,7 @@ export function getLeaderboard( options ) { select, filterQuery, } = options; + const { getItems, getItemsError, isResolving } = select( STORE_NAME ); const response = { isRequesting: false, @@ -49,7 +63,10 @@ export function getLeaderboard( options ) { // Disable eslint rule requiring `getItems` to be defined below because the next two statements // depend on `getItems` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const leaderboards = getItems( endpoint, leaderboardQuery ); + const leaderboards = getItems< 'leaderboards' >( + endpoint, + leaderboardQuery + ); if ( isResolving( 'getItems', [ endpoint, leaderboardQuery ] ) ) { return { ...response, isRequesting: true }; @@ -69,15 +86,15 @@ export function getLeaderboard( options ) { * @param {Object} options Query options. * @return {Object} Object containing API request information and the matching items. */ -export function searchItemsByString( - selector, - endpoint, - search, - options = {} +export function searchItemsByString< T extends ItemType >( + selector: ItemsSelector, + endpoint: T, + search: string[], + options: Query = {} ) { const { getItems, getItemsError, isResolving } = selector; - const items = {}; + const items: Record< number, ItemInfer< T > | undefined > = {}; let isRequesting = false; let isError = false; search.forEach( ( searchWord ) => { @@ -86,7 +103,7 @@ export function searchItemsByString( per_page: 10, ...options, }; - const newItems = getItems( endpoint, query ); + const newItems = getItems< T >( endpoint, query ); newItems.forEach( ( item, id ) => { items[ id ] = item; } ); @@ -111,11 +128,12 @@ export function searchItemsByString( * @param {Object} query Query for item totals count. * @return {string} Resource name for item totals. */ -export function getTotalCountResourceName( itemType, query ) { +export function getTotalCountResourceName( itemType: string, query: Query ) { // Disable eslint rule because we're using this spread to omit properties // that don't affect item totals count results. // eslint-disable-next-line no-unused-vars, camelcase + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { _fields, page, per_page, ...totalsQuery } = query; - return getResourceName( 'total-' + itemType, totalsQuery ); + return getResourceName( 'total-' + itemType, { ...totalsQuery } ); } diff --git a/packages/js/data/src/types/wp-data.ts b/packages/js/data/src/types/wp-data.ts index 46b5287fc6b..f6deb9b4f39 100644 --- a/packages/js/data/src/types/wp-data.ts +++ b/packages/js/data/src/types/wp-data.ts @@ -4,18 +4,18 @@ // [wp.data.getSelectors](https://github.com/WordPress/gutenberg/blob/319deee5f4d4838d6bc280e9e2be89c7f43f2509/packages/data/src/store/index.js#L16-L20) // [selector.js](https://github.com/WordPress/gutenberg/blob/trunk/packages/data/src/redux-store/metadata/selectors.js#L48-L52) export type WPDataSelectors = { - getIsResolving: ( selector: string, args?: string[] ) => boolean; - hasStartedResolution: ( selector: string, args?: string[] ) => boolean; - hasFinishedResolution: ( selector: string, args?: string[] ) => boolean; - isResolving: ( selector: string, args?: string[] ) => boolean; + getIsResolving: ( selector: string, args?: unknown[] ) => boolean; + hasStartedResolution: ( selector: string, args?: unknown[] ) => boolean; + hasFinishedResolution: ( selector: string, args?: unknown[] ) => boolean; + isResolving: ( selector: string, args?: unknown[] ) => boolean; getCachedResolvers: () => unknown; }; // [wp.data.getActions](https://github.com/WordPress/gutenberg/blob/319deee5f4d4838d6bc280e9e2be89c7f43f2509/packages/data/src/store/index.js#L31-L35) // [actions.js](https://github.com/WordPress/gutenberg/blob/aa2bed9010aa50467cb43063e370b70a91591e9b/packages/data/src/redux-store/metadata/actions.js) export type WPDataActions = { - startResolution: ( selector: string, args?: string[] ) => void; - finishResolution: ( selector: string, args?: string[] ) => void; + startResolution: ( selector: string, args?: unknown[] ) => void; + finishResolution: ( selector: string, args?: unknown[] ) => void; invalidateResolution: ( selector: string ) => void; invalidateResolutionForStore: ( selector: string ) => void; invalidateResolutionForStoreSelector: ( selector: string ) => void; diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts index bc0cd43d527..feb90c52175 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts +++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts @@ -27,7 +27,7 @@ export const useCreateProductByType = () => { setIsRequesting( true ); try { const data: { - id?: string; + id?: number; } = await createProductFromTemplate( { template_name: type, diff --git a/plugins/woocommerce/changelog/dev-migrate-item-store-to-ts b/plugins/woocommerce/changelog/dev-migrate-item-store-to-ts new file mode 100644 index 00000000000..f21ebe746e8 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-migrate-item-store-to-ts @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: No logic or UI changes. + +