Add data store attribute terms (#33721)

* Add product attribute terms store

* Add ability to add params to URL

* Allow IdQuery for all crud functions

* Add tests around item parent keys

* Add tests around new utils

* Throw error when not all params are replaced in REST URL

* Add require attributes for terms

* Allow urlParameters to be specified

* Use namespace to detect url parameters

* Fix up selector return after applying namespace

* Clean queries to prevent sending URL param data to endpoint

* Add tests around new utils

* Remove unused method import

* Remove urlParameters argument no longer being used

* Add changelog entry
This commit is contained in:
Joshua T Flowers 2022-07-21 12:28:30 -04:00 committed by GitHub
parent ecd17484bb
commit 4c1a82dc26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 577 additions and 84 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product attribute terms data store

View File

@ -1,15 +1,15 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { cleanQuery, getUrlParameters, getRestPath, parseId } from './utils';
import CRUD_ACTIONS from './crud-actions';
import TYPES from './action-types';
import { IdType, Item, ItemQuery } from './types';
import { IdType, IdQuery, Item, ItemQuery } from './types';
type ResolverOptions = {
resourceName: string;
@ -25,50 +25,53 @@ export function createItemError( query: Partial< ItemQuery >, error: unknown ) {
};
}
export function createItemSuccess( id: IdType, item: Item ) {
export function createItemSuccess( key: IdType, item: Item ) {
return {
type: TYPES.CREATE_ITEM_SUCCESS as const,
id,
key,
item,
};
}
export function deleteItemError( id: IdType, error: unknown ) {
export function deleteItemError( key: IdType, error: unknown ) {
return {
type: TYPES.DELETE_ITEM_ERROR as const,
id,
key,
error,
errorType: CRUD_ACTIONS.DELETE_ITEM,
};
}
export function deleteItemSuccess( id: IdType, force: boolean, item: Item ) {
export function deleteItemSuccess( key: IdQuery, force: boolean, item: Item ) {
return {
type: TYPES.DELETE_ITEM_SUCCESS as const,
id,
key,
force,
item,
};
}
export function getItemError( id: unknown, error: unknown ) {
export function getItemError( key: IdType, error: unknown ) {
return {
type: TYPES.GET_ITEM_ERROR as const,
id,
key,
error,
errorType: CRUD_ACTIONS.GET_ITEM,
};
}
export function getItemSuccess( id: IdType, item: Item ) {
export function getItemSuccess( key: IdType, item: Item ) {
return {
type: TYPES.GET_ITEM_SUCCESS as const,
id,
key,
item,
};
}
export function getItemsError( query: unknown, error: unknown ) {
export function getItemsError(
query: Partial< ItemQuery > | undefined,
error: unknown
) {
return {
type: TYPES.GET_ITEMS_ERROR as const,
query,
@ -77,27 +80,32 @@ export function getItemsError( query: unknown, error: unknown ) {
};
}
export function getItemsSuccess( query: unknown, items: Item[] ) {
export function getItemsSuccess(
query: Partial< ItemQuery > | undefined,
items: Item[],
urlParameters: IdType[]
) {
return {
type: TYPES.GET_ITEMS_SUCCESS as const,
items,
query,
urlParameters,
};
}
export function updateItemError( id: unknown, error: unknown ) {
export function updateItemError( key: IdType, error: unknown ) {
return {
type: TYPES.UPDATE_ITEM_ERROR as const,
id,
key,
error,
errorType: CRUD_ACTIONS.UPDATE_ITEM,
};
}
export function updateItemSuccess( id: IdType, item: Item ) {
export function updateItemSuccess( key: IdType, item: Item ) {
return {
type: TYPES.UPDATE_ITEM_SUCCESS as const,
id,
key,
item,
};
}
@ -107,13 +115,20 @@ export const createDispatchActions = ( {
resourceName,
}: ResolverOptions ) => {
const createItem = function* ( query: Partial< ItemQuery > ) {
const urlParameters = getUrlParameters( namespace, query );
try {
const item: Item = yield apiFetch( {
path: addQueryArgs( namespace, query ),
path: getRestPath(
namespace,
cleanQuery( query, namespace ),
urlParameters
),
method: 'POST',
} );
const { key } = parseId( item.id, urlParameters );
yield createItemSuccess( item.id, item );
yield createItemSuccess( key, item );
return item;
} catch ( error ) {
yield createItemError( query, error );
@ -121,32 +136,49 @@ export const createDispatchActions = ( {
}
};
const deleteItem = function* ( id: IdType, force = true ) {
const deleteItem = function* ( idQuery: IdQuery, force = true ) {
const urlParameters = getUrlParameters( namespace, idQuery );
const { id, key } = parseId( idQuery, urlParameters );
try {
const item: Item = yield apiFetch( {
path: addQueryArgs( `${ namespace }/${ id }`, { force } ),
path: getRestPath(
`${ namespace }/${ id }`,
{ force },
urlParameters
),
method: 'DELETE',
} );
yield deleteItemSuccess( id, force, item );
yield deleteItemSuccess( key, force, item );
return item;
} catch ( error ) {
yield deleteItemError( id, error );
yield deleteItemError( key, error );
throw error;
}
};
const updateItem = function* ( id: IdType, query: Partial< ItemQuery > ) {
const updateItem = function* (
idQuery: IdQuery,
query: Partial< ItemQuery >
) {
const urlParameters = getUrlParameters( namespace, idQuery );
const { id, key } = parseId( idQuery, urlParameters );
try {
const item: Item = yield apiFetch( {
path: addQueryArgs( `${ namespace }/${ id }`, query ),
path: getRestPath(
`${ namespace }/${ id }`,
cleanQuery( query, namespace ),
urlParameters
),
method: 'PUT',
} );
yield updateItemSuccess( item.id, item );
yield updateItemSuccess( key, item );
return item;
} catch ( error ) {
yield updateItemError( query, error );
yield updateItemError( key, error );
throw error;
}
};

View File

@ -36,7 +36,11 @@ export const createCrudDataStore = ( {
pluralResourceName,
namespace,
} );
const selectors = createSelectors( { resourceName, pluralResourceName } );
const selectors = createSelectors( {
resourceName,
pluralResourceName,
namespace,
} );
registerStore( storeName, {
reducer: reducer as Reducer< ResourceState >,

View File

@ -8,6 +8,7 @@ import { Reducer } from 'redux';
*/
import { Actions } from './actions';
import CRUD_ACTIONS from './crud-actions';
import { getKey } from './utils';
import { getResourceName } from '../utils';
import { IdType, Item, ItemQuery } from './types';
import { TYPES } from './action-types';
@ -56,25 +57,25 @@ export const createReducer = () => {
...state,
data: {
...itemData,
[ payload.id ]: {
...( itemData[ payload.id ] || {} ),
[ payload.key ]: {
...( itemData[ payload.key ] || {} ),
...payload.item,
},
},
};
case TYPES.DELETE_ITEM_SUCCESS:
const itemIds = Object.keys( state.data );
const nextData = itemIds.reduce< Data >(
( items: Data, id: string ) => {
if ( id !== payload.id.toString() ) {
items[ id ] = state.data[ id ];
const itemKeys = Object.keys( state.data );
const nextData = itemKeys.reduce< Data >(
( items: Data, key: string ) => {
if ( key !== payload.key.toString() ) {
items[ key ] = state.data[ key ];
return items;
}
if ( payload.force ) {
return items;
}
items[ id ] = payload.item;
items[ key ] = payload.item;
return items;
},
{} as Data
@ -93,7 +94,7 @@ export const createReducer = () => {
errors: {
...state.errors,
[ getResourceName( payload.errorType, {
id: payload.id,
key: payload.key,
} ) ]: payload.error,
},
};
@ -104,9 +105,10 @@ export const createReducer = () => {
const nextResources = payload.items.reduce<
Record< string, Item >
>( ( result, item ) => {
ids.push( item.id );
result[ item.id ] = {
...( state.data[ item.id ] || {} ),
const key = getKey( item.id, payload.urlParameters );
ids.push( key );
result[ key ] = {
...( state.data[ key ] || {} ),
...item,
};
return result;

View File

@ -6,14 +6,15 @@ import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { cleanQuery, getUrlParameters, getRestPath, parseId } from './utils';
import {
getItemError,
getItemSuccess,
getItemsError,
getItemsSuccess,
} from './actions';
import { IdQuery, Item, ItemQuery } from './types';
import { request } from '../utils';
import { Item, ItemQuery } from './types';
type ResolverOptions = {
resourceName: string;
@ -26,25 +27,32 @@ export const createResolvers = ( {
pluralResourceName,
namespace,
}: ResolverOptions ) => {
const getItem = function* ( id: number ) {
const getItem = function* ( idQuery: IdQuery ) {
const urlParameters = getUrlParameters( namespace, idQuery );
const { id, key } = parseId( idQuery, urlParameters );
try {
const item: Item = yield apiFetch( {
path: `${ namespace }/${ id }`,
path: getRestPath(
`${ namespace }/${ id }`,
{},
urlParameters
),
method: 'GET',
} );
yield getItemSuccess( item.id, item );
yield getItemSuccess( key, item );
return item;
} catch ( error ) {
yield getItemError( id, error );
yield getItemError( key, error );
throw error;
}
};
const getItems = function* ( query?: Partial< ItemQuery > ) {
// Require ID when requesting specific fields to later update the resource data.
const resourceQuery = query ? { ...query } : {};
const urlParameters = getUrlParameters( namespace, query || {} );
const resourceQuery = cleanQuery( query || {}, namespace );
// Require ID when requesting specific fields to later update the resource data.
if (
resourceQuery &&
resourceQuery._fields &&
@ -54,12 +62,13 @@ export const createResolvers = ( {
}
try {
const path = getRestPath( namespace, {}, urlParameters );
const { items }: { items: Item[] } = yield request<
ItemQuery,
Item
>( namespace, resourceQuery );
>( path, resourceQuery );
yield getItemsSuccess( query, items );
yield getItemsSuccess( query, items, urlParameters );
return items;
} catch ( error ) {
yield getItemsError( query, error );

View File

@ -6,14 +6,16 @@ import createSelector from 'rememo';
/**
* Internal dependencies
*/
import { applyNamespace, getUrlParameters, parseId } from './utils';
import { getResourceName } from '../utils';
import { IdType, Item, ItemQuery } from './types';
import { IdQuery, IdType, Item, ItemQuery } from './types';
import { ResourceState } from './reducer';
import CRUD_ACTIONS from './crud-actions';
type SelectorOptions = {
resourceName: string;
pluralResourceName: string;
namespace: string;
};
export const getItemCreateError = (
@ -24,17 +26,35 @@ export const getItemCreateError = (
return state.errors[ itemQuery ];
};
export const getItemDeleteError = ( state: ResourceState, id: IdType ) => {
const itemQuery = getResourceName( CRUD_ACTIONS.DELETE_ITEM, { id } );
export const getItemDeleteError = (
state: ResourceState,
idQuery: IdQuery,
namespace: string
) => {
const urlParameters = getUrlParameters( namespace, idQuery );
const { key } = parseId( idQuery, urlParameters );
const itemQuery = getResourceName( CRUD_ACTIONS.DELETE_ITEM, { key } );
return state.errors[ itemQuery ];
};
export const getItem = ( state: ResourceState, id: IdType ) => {
return state.data[ id ];
export const getItem = (
state: ResourceState,
idQuery: IdQuery,
namespace: string
) => {
const urlParameters = getUrlParameters( namespace, idQuery );
const { key } = parseId( idQuery, urlParameters );
return state.data[ key ];
};
export const getItemError = ( state: ResourceState, id: IdType ) => {
const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } );
export const getItemError = (
state: ResourceState,
idQuery: IdQuery,
namespace: string
) => {
const urlParameters = getUrlParameters( namespace, idQuery );
const { key } = parseId( idQuery, urlParameters );
const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEM, { key } );
return state.errors[ itemQuery ];
};
@ -67,11 +87,13 @@ export const getItems = createSelector(
} );
}
return ids
const data = ids
.map( ( id: IdType ) => {
return state.data[ id ];
} )
.filter( ( item ) => item !== undefined );
return data;
},
( state, query ) => {
const itemQuery = getResourceName(
@ -81,6 +103,7 @@ export const getItems = createSelector(
const ids = state.items[ itemQuery ]
? state.items[ itemQuery ].data
: undefined;
return [
state.items[ itemQuery ],
...( ids || [] ).map( ( id: string ) => {
@ -95,22 +118,47 @@ export const getItemsError = ( state: ResourceState, query?: ItemQuery ) => {
return state.errors[ itemQuery ];
};
export const getItemUpdateError = ( state: ResourceState, id: IdType ) => {
const itemQuery = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, { id } );
export const getItemUpdateError = (
state: ResourceState,
idQuery: IdQuery,
urlParameters: IdType[]
) => {
const params = parseId( idQuery, urlParameters );
const { key } = params;
const itemQuery = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, {
key,
params,
} );
return state.errors[ itemQuery ];
};
export const createSelectors = ( {
resourceName,
pluralResourceName,
namespace,
}: SelectorOptions ) => {
return {
[ `get${ resourceName }` ]: getItem,
[ `get${ resourceName }Error` ]: getItemError,
[ `get${ pluralResourceName }` ]: getItems,
[ `get${ pluralResourceName }Error` ]: getItemsError,
[ `get${ resourceName }CreateError` ]: getItemCreateError,
[ `get${ resourceName }DeleteError` ]: getItemDeleteError,
[ `get${ resourceName }UpdateError` ]: getItemUpdateError,
[ `get${ resourceName }` ]: applyNamespace( getItem, namespace ),
[ `get${ resourceName }Error` ]: applyNamespace(
getItemError,
namespace
),
[ `get${ pluralResourceName }` ]: applyNamespace( getItems, namespace ),
[ `get${ pluralResourceName }Error` ]: applyNamespace(
getItemsError,
namespace
),
[ `get${ resourceName }CreateError` ]: applyNamespace(
getItemCreateError,
namespace
),
[ `get${ resourceName }DeleteError` ]: applyNamespace(
getItemDeleteError,
namespace
),
[ `get${ resourceName }UpdateError` ]: applyNamespace(
getItemUpdateError,
namespace
),
};
};

View File

@ -44,7 +44,7 @@ describe( 'crud reducer', () => {
const state = reducer( initialState, {
type: TYPES.GET_ITEM_SUCCESS,
id: update.id,
key: update.id,
item: update,
} );
@ -67,6 +67,7 @@ describe( 'crud reducer', () => {
type: TYPES.GET_ITEMS_SUCCESS,
items,
query,
urlParameters: [],
} );
const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query );
@ -79,6 +80,26 @@ describe( 'crud reducer', () => {
expect( state.data[ 2 ] ).toEqual( items[ 1 ] );
} );
it( 'should handle GET_ITEMS_SUCCESS with urlParameters', () => {
const items: Item[] = [
{ id: 1, name: 'Yum!' },
{ id: 2, name: 'Dynamite!' },
];
const query: Partial< ItemQuery > = { status: 'draft' };
const state = reducer( defaultState, {
type: TYPES.GET_ITEMS_SUCCESS,
items,
query,
urlParameters: [ 5 ],
} );
const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query );
expect( state.items[ resourceName ].data ).toHaveLength( 2 );
expect( state.data[ '5/1' ] ).toEqual( items[ 0 ] );
expect( state.data[ '5/2' ] ).toEqual( items[ 1 ] );
} );
it( 'GET_ITEMS_SUCCESS should not remove previously added fields, only update new ones', () => {
const initialState: ResourceState = {
...defaultState,
@ -102,6 +123,7 @@ describe( 'crud reducer', () => {
type: TYPES.GET_ITEMS_SUCCESS,
items,
query,
urlParameters: [],
} );
const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query );
@ -133,12 +155,12 @@ describe( 'crud reducer', () => {
} );
it( 'should handle GET_ITEM_ERROR', () => {
const id = 3;
const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } );
const key = 3;
const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { key } );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.GET_ITEM_ERROR,
id,
key,
error,
errorType: CRUD_ACTIONS.GET_ITEM,
} );
@ -147,12 +169,12 @@ describe( 'crud reducer', () => {
} );
it( 'should handle GET_ITEM_ERROR', () => {
const id = 3;
const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } );
const key = 3;
const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { key } );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.GET_ITEM_ERROR,
id,
key,
error,
errorType: CRUD_ACTIONS.GET_ITEM,
} );
@ -175,14 +197,14 @@ describe( 'crud reducer', () => {
} );
it( 'should handle UPDATE_ITEM_ERROR', () => {
const id = 2;
const key = 2;
const resourceName = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, {
id,
key,
} );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.UPDATE_ITEM_ERROR,
id,
key,
error,
errorType: CRUD_ACTIONS.UPDATE_ITEM,
} );
@ -212,7 +234,7 @@ describe( 'crud reducer', () => {
const state = reducer( initialState, {
type: TYPES.UPDATE_ITEM_SUCCESS,
id: item.id,
key: item.id,
item,
} );
@ -234,7 +256,7 @@ describe( 'crud reducer', () => {
const state = reducer( defaultState, {
type: TYPES.CREATE_ITEM_SUCCESS,
id: item.id,
key: item.id,
item,
} );
@ -267,13 +289,13 @@ describe( 'crud reducer', () => {
let state = reducer( initialState, {
type: TYPES.DELETE_ITEM_SUCCESS,
id: item1Updated.id,
key: item1Updated.id,
item: item1Updated,
force: true,
} );
state = reducer( state, {
type: TYPES.DELETE_ITEM_SUCCESS,
id: item2Updated.id,
key: item2Updated.id,
item: item2Updated,
force: false,
} );
@ -284,14 +306,14 @@ describe( 'crud reducer', () => {
} );
it( 'should handle DELETE_ITEM_ERROR', () => {
const id = 2;
const key = 2;
const resourceName = getResourceName( CRUD_ACTIONS.DELETE_ITEM, {
id,
key,
} );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.DELETE_ITEM_ERROR,
id,
key,
error,
errorType: CRUD_ACTIONS.DELETE_ITEM,
} );

View File

@ -6,6 +6,7 @@ import { createSelectors } from '../selectors';
const selectors = createSelectors( {
resourceName: 'Product',
pluralResourceName: 'Products',
namespace: '',
} );
describe( 'crud selectors', () => {

View File

@ -0,0 +1,116 @@
/**
* Internal dependencies
*/
import {
applyNamespace,
cleanQuery,
getKey,
getNamespaceKeys,
getRestPath,
getUrlParameters,
parseId,
} from '../utils';
describe( 'utils', () => {
it( 'should get the rest path when no parameters are given', () => {
const path = getRestPath( 'test/path', {}, [] );
expect( path ).toEqual( 'test/path' );
} );
it( 'should replace the rest path parameters when provided', () => {
const path = getRestPath( 'test/{parent}/path', {}, [ 'insert' ] );
expect( path ).toEqual( 'test/insert/path' );
} );
it( 'should replace the rest path parameters when multiple are provided', () => {
const path = getRestPath( 'test/{parent}/{other}/path', {}, [
'insert',
'next',
] );
expect( path ).toEqual( 'test/insert/next/path' );
} );
it( 'should throw an error when not all parameters are replaced', () => {
expect( () =>
getRestPath( 'test/{parent}/{other}/path', {}, [ 'insert' ] )
).toThrow( Error );
} );
it( 'should get the key when no parent is provided', () => {
const key = getKey( 3, [] );
expect( key ).toEqual( 3 );
} );
it( 'should get the key when a parent is provided', () => {
const key = getKey( 3, [ 5 ] );
expect( key ).toEqual( '5/3' );
} );
it( 'should get the correct ID information when only an ID is given', () => {
const parsed = parseId( 3 );
expect( parsed.key ).toEqual( 3 );
expect( parsed.id ).toEqual( 3 );
} );
it( 'should get the correct ID information when an object is given', () => {
const parsed = parseId( { id: 3, parent_id: 5 }, [ 5 ] );
expect( parsed.key ).toEqual( '5/3' );
expect( parsed.id ).toEqual( 3 );
} );
it( 'should apply the namespace as an argument to a given function', () => {
const namespace = 'test/namespace';
const mockedCallback = jest.fn();
const wrappedFunction = applyNamespace( mockedCallback, namespace );
wrappedFunction( 'a' );
expect( mockedCallback ).toBeCalledTimes( 1 );
expect( mockedCallback ).toBeCalledWith( 'a', 'test/namespace' );
} );
it( 'should get the keys from a namespace', () => {
const namespace = 'test/{first}/namespace/{second}';
const keys = getNamespaceKeys( namespace );
expect( keys.length ).toBe( 2 );
expect( keys[ 0 ] ).toBe( 'first' );
expect( keys[ 1 ] ).toBe( 'second' );
} );
it( 'should return an empty array from a namespace without params', () => {
const namespace = 'test/namespace';
const keys = getNamespaceKeys( namespace );
expect( keys.length ).toBe( 0 );
} );
it( 'should get the URL parameters given a namespace and query', () => {
const namespace = 'test/{my_attribute}/namespace/{other_attribute}';
const params = getUrlParameters( namespace, {
id: 5,
my_attribute: 'flavortown',
other_attribute: 'donkeysauce',
} );
expect( params.length ).toBe( 2 );
expect( params[ 0 ] ).toBe( 'flavortown' );
expect( params[ 1 ] ).toBe( 'donkeysauce' );
} );
it( 'should return an empty array when no namespace variables are matched', () => {
const namespace = 'test/{my_attribute}/namespace';
const params = getUrlParameters( namespace, {
id: 5,
different_attribute: 'flavortown',
} );
expect( params.length ).toBe( 0 );
} );
it( 'should remove namespace params from a given query', () => {
const namespace = 'test/{my_attribute}/namespace';
const query = {
other_attribute: 'a',
my_attribute: 'b',
};
const params = cleanQuery( query, namespace );
expect( params.other_attribute ).toBe( 'a' );
expect( params.my_attribute ).toBeUndefined();
} );
} );

View File

@ -14,6 +14,13 @@ import {
export type IdType = number | string;
export type IdQuery =
| IdType
| {
id: IdType;
[ key: string ]: IdType;
};
export type Item = {
id: IdType;
[ key: string ]: unknown;
@ -21,6 +28,11 @@ export type Item = {
export type ItemQuery = BaseQueryParams & {
[ key: string ]: unknown;
parent_id?: IdType;
};
export type Params = {
[ key: string ]: IdType;
};
type WithRequiredProperty< Type, Key extends keyof Type > = Type & {

View File

@ -0,0 +1,162 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import { IdQuery, IdType, ItemQuery } from './types';
/**
* Get a REST path given a template path and URL params.
*
* @param templatePath Path with variable names.
* @param query Item query.
* @param parameters Array of items to replace in the templatePath.
* @return string REST path.
*/
export const getRestPath = (
templatePath: string,
query: Partial< ItemQuery >,
parameters: IdType[]
) => {
let path = templatePath;
path.match( /{(.*?)}/g )?.forEach( ( str, i ) => {
path = path.replace( str, parameters[ i ].toString() );
} );
const regex = new RegExp( /{|}/ );
if ( regex.test( path.toString() ) ) {
throw new Error( 'Not all URL parameters were replaced' );
}
return addQueryArgs( path, query );
};
/**
* Get a key from an item ID and optional parent.
*
* @param query Item Query.
* @param urlParameters Parameters used for URL.
* @return string
*/
export const getKey = ( query: IdQuery, urlParameters: IdType[] = [] ) => {
const id =
typeof query === 'string' || typeof query === 'number'
? query
: query.id;
if ( ! urlParameters.length ) {
return id;
}
let prefix = '';
urlParameters.forEach( ( param ) => {
prefix = param + '/';
} );
return `${ prefix }${ id }`;
};
/**
* Parse an ID query into a ID string.
*
* @param query Id Query
* @return string ID.
*/
export const parseId = ( query: IdQuery, urlParameters: IdType[] = [] ) => {
if ( typeof query === 'string' || typeof query === 'number' ) {
return {
id: query,
key: query,
};
}
return {
id: query.id,
key: getKey( query, urlParameters ),
};
};
/**
* Create a new function that adds in the namespace.
*
* @param fn Function to wrap.
* @param namespace Namespace to pass to last argument of function.
* @return Wrapped function
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const applyNamespace = < T extends ( ...args: any[] ) => unknown >(
fn: T,
namespace: string
) => {
return ( ...args: Parameters< T > ) => {
return fn( ...args, namespace );
};
};
/**
* Get the key names from a namespace string.
*
* @param namespace Namespace to get keys from.
* @return Array of keys.
*/
export const getNamespaceKeys = ( namespace: string ) => {
const keys: string[] = [];
namespace.match( /{(.*?)}/g )?.forEach( ( match ) => {
const key = match.substr( 1, match.length - 2 );
keys.push( key );
} );
return keys;
};
/**
* Get URL parameters from namespace and provided query.
*
* @param namespace Namespace string to replace params in.
* @param query Query object with key values.
* @return Array of URL parameter values.
*/
export const getUrlParameters = (
namespace: string,
query: IdQuery | Partial< ItemQuery >
) => {
if ( typeof query !== 'object' ) {
return [];
}
const params: IdType[] = [];
const keys = getNamespaceKeys( namespace );
keys.forEach( ( key ) => {
if ( query.hasOwnProperty( key ) ) {
params.push( query[ key ] as IdType );
}
} );
return params;
};
/**
* Clean a query of all namespaced params.
*
* @param query Query to clean.
* @param namespace
* @return Cleaned query object.
*/
export const cleanQuery = (
query: Partial< ItemQuery >,
namespace: string
) => {
const cleaned = { ...query };
const keys = getNamespaceKeys( namespace );
keys.forEach( ( key ) => {
delete cleaned[ key ];
} );
return cleaned;
};

View File

@ -22,6 +22,7 @@ export { EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME } from './product-attributes
export { EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME } from './product-shipping-classes';
export { EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME } from './shipping-zones';
export { EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME } from './product-tags';
export { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME } from './product-attribute-terms';
export { PaymentGateway } from './payment-gateways/types';
// Export hooks
@ -97,6 +98,7 @@ import type { EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME } from './product-attri
import type { EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME } from './product-shipping-classes';
import type { EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME } from './shipping-zones';
import type { EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME } from './product-tags';
import type { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME } from './product-attribute-terms';
export type WCDataStoreName =
| typeof REVIEWS_STORE_NAME
@ -114,6 +116,7 @@ export type WCDataStoreName =
| typeof PRODUCTS_STORE_NAME
| typeof ORDERS_STORE_NAME
| typeof EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME
| typeof EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
| typeof EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME
| typeof EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME
| typeof EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME;
@ -132,6 +135,7 @@ import { ProductAttributeSelectors } from './product-attributes/types';
import { ProductShippingClassSelectors } from './product-shipping-classes/types';
import { ShippingZonesSelectors } from './shipping-zones/types';
import { ProductTagSelectors } from './product-tags/types';
import { ProductAttributeTermsSelectors } from './product-attribute-terms/types';
// As we add types to all the package selectors we can fill out these unknown types with real ones. See one
// of the already typed selectors for an example of how you can do this.
@ -167,6 +171,8 @@ export type WCSelectorType< T > = T extends typeof REVIEWS_STORE_NAME
? ProductShippingClassSelectors
: T extends typeof EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME
? ProductTagSelectors
: T extends typeof EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
? ProductAttributeTermsSelectors
: T extends typeof ORDERS_STORE_NAME
? OrdersSelectors
: T extends typeof EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME
@ -181,6 +187,7 @@ export interface WCDataSelector {
export { ActionDispatchers as PluginsStoreActions } from './plugins/actions';
export { ActionDispatchers as ProductAttributesActions } from './product-attributes/types';
export { ActionDispatchers as ProductTagsActions } from './product-tags/types';
export { ActionDispatchers as ProductAttributeTermsActions } from './product-attribute-terms/types';
export { ActionDispatchers as ProductsStoreActions } from './products/actions';
export { ActionDispatchers as ProductShippingClassesActions } from './product-shipping-classes/types';
export { ActionDispatchers as ShippingZonesActions } from './shipping-zones/types';

View File

@ -0,0 +1,4 @@
export const STORE_NAME = 'wc/admin/products/attributes/terms';
export const WC_PRODUCT_ATTRIBUTE_TERMS_NAMESPACE =
'/wc/v3/products/attributes/{attribute_id}/terms';

View File

@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import { STORE_NAME, WC_PRODUCT_ATTRIBUTE_TERMS_NAMESPACE } from './constants';
import { createCrudDataStore } from '../crud';
createCrudDataStore( {
storeName: STORE_NAME,
resourceName: 'ProductAttributeTerm',
pluralResourceName: 'ProductAttributeTerms',
namespace: WC_PRODUCT_ATTRIBUTE_TERMS_NAMESPACE,
} );
export const EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME = STORE_NAME;

View File

@ -0,0 +1,56 @@
/**
* External dependencies
*/
import { DispatchFromMap } from '@automattic/data-stores';
/**
* Internal dependencies
*/
import { CrudActions, CrudSelectors } from '../crud/types';
type ProductAttributeTerm = {
id: number;
slug: string;
name: string;
description: string;
menu_order: number;
count: number;
};
type Query = {
context: string;
page: number;
per_page: number;
search: string;
exclude: number[];
include: number[];
offset: number;
order: string;
orderby: string;
hide_empty: boolean;
product: number;
slug: string;
};
type ReadOnlyProperties = 'id' | 'count';
type MutableProperties = Partial<
Omit< ProductAttributeTerm, ReadOnlyProperties >
>;
type ProductAttributeTermActions = CrudActions<
'ProductAttributeTerm',
ProductAttributeTerm,
MutableProperties,
'name'
>;
export type ProductAttributeTermsSelectors = CrudSelectors<
'ProductAttributeTerm',
'ProductAttributeTerms',
ProductAttributeTerm,
Query,
MutableProperties
>;
export type ActionDispatchers = DispatchFromMap< ProductAttributeTermActions >;