* Create wp.data folder

* order panel

* fix isUnboundedRequest with Categories

* products report

* products table

* indicators and leaderboards

* orders and stock panels

* utils

* tests

* save

* updateStock

* remove wc-api items

* updateItems -> setItems
This commit is contained in:
Paul Sealock 2020-08-21 11:37:41 +12:00 committed by GitHub
parent 2f650b74a0
commit 74e8d7622e
25 changed files with 395 additions and 358 deletions

View File

@ -4,18 +4,21 @@
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import PropTypes from 'prop-types';
import { Card, EmptyTable, TableCard } from '@woocommerce/components';
import { getPersistedQuery } from '@woocommerce/navigation';
import { getFilterQuery, SETTINGS_STORE_NAME } from '@woocommerce/data';
import {
getFilterQuery,
getLeaderboard,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
/**
* Internal dependencies
*/
import { getLeaderboard } from '../../../wc-api/items/utils';
import ReportError from '../report-error';
import sanitizeHTML from '../../../lib/sanitize-html';
import withSelect from '../../../wc-api/with-select';
import './style.scss';
export class Leaderboard extends Component {

View File

@ -4,17 +4,18 @@
import { __, _n } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import { map } from 'lodash';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
import { formatValue } from '@woocommerce/number';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
import CategoryBreacrumbs from './breadcrumbs';
import ReportTable from '../../components/report-table';
import withSelect from '../../../wc-api/with-select';
import { CurrencyContext } from '../../../lib/currency-context';
class CategoriesReportTable extends Component {
@ -226,8 +227,8 @@ export default compose(
return {};
}
const { getItems, getItemsError, isGetItemsRequesting } = select(
'wc-api'
const { getItems, getItemsError, isResolving } = select(
ITEMS_STORE_NAME
);
const tableQuery = {
per_page: -1,
@ -237,10 +238,10 @@ export default compose(
const isCategoriesError = Boolean(
getItemsError( 'categories', tableQuery )
);
const isCategoriesRequesting = isGetItemsRequesting(
const isCategoriesRequesting = isResolving( 'getItems', [
'categories',
tableQuery
);
tableQuery,
] );
return {
categories,

View File

@ -3,17 +3,17 @@
*/
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import PropTypes from 'prop-types';
import { find } from 'lodash';
import { getQuery, getSearchWords } from '@woocommerce/navigation';
import { searchItemsByString } from '@woocommerce/data';
/**
* Internal dependencies
*/
import './style.scss';
import ReportError from '../components/report-error';
import { searchItemsByString } from '../../wc-api/items/utils';
import withSelect from '../../wc-api/with-select';
import {
CurrencyContext,
getFilteredCurrencyInstance,

View File

@ -5,6 +5,8 @@ import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
import { withSelect } from '@wordpress/data';
/**
* Internal dependencies
@ -16,7 +18,6 @@ import ReportChart from '../../components/report-chart';
import ReportError from '../../components/report-error';
import ReportSummary from '../../components/report-summary';
import VariationsReportTable from './table-variations';
import withSelect from '../../../wc-api/with-select';
import ReportFilters from '../../components/report-filters';
class ProductsReport extends Component {
@ -151,8 +152,8 @@ export default compose(
};
}
const { getItems, isGetItemsRequesting, getItemsError } = select(
'wc-api'
const { getItems, isResolving, getItemsError } = select(
ITEMS_STORE_NAME
);
if ( isSingleProductView ) {
const productId = parseInt( query.products, 10 );
@ -163,10 +164,10 @@ export default compose(
products &&
products.get( productId ) &&
products.get( productId ).type === 'variable';
const isProductsRequesting = isGetItemsRequesting(
const isProductsRequesting = isResolving( 'getItems', [
'products',
includeArgs
);
includeArgs,
] );
const isProductsError = Boolean(
getItemsError( 'products', includeArgs )
);

View File

@ -5,11 +5,13 @@ import { __, _n, _x, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { decodeEntities } from '@wordpress/html-entities';
import { withSelect } from '@wordpress/data';
import { map } from 'lodash';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { Link, Tag } from '@woocommerce/components';
import { formatValue } from '@woocommerce/number';
import { getAdminLink, getSetting } from '@woocommerce/wc-admin-settings';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
@ -17,7 +19,6 @@ import { getAdminLink, getSetting } from '@woocommerce/wc-admin-settings';
import CategoryBreacrumbs from '../categories/breadcrumbs';
import { isLowStock } from './utils';
import ReportTable from '../../components/report-table';
import withSelect from '../../../wc-api/with-select';
import { CurrencyContext } from '../../../lib/currency-context';
import './style.scss';
@ -381,8 +382,8 @@ export default compose(
return {};
}
const { getItems, getItemsError, isGetItemsRequesting } = select(
'wc-api'
const { getItems, getItemsError, isResolving } = select(
ITEMS_STORE_NAME
);
const tableQuery = {
per_page: -1,
@ -390,7 +391,10 @@ export default compose(
const categories = getItems( 'categories', tableQuery );
const isError = Boolean( getItemsError( 'categories', tableQuery ) );
const isLoading = isGetItemsRequesting( 'categories', tableQuery );
const isLoading = isResolving( 'getItems', [
'categories',
tableQuery,
] );
return { categories, isError, isRequesting: isLoading };
} )

View File

@ -6,14 +6,14 @@ import { Fragment, useState } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
import { SelectControl } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
import {
EllipsisMenu,
MenuItem,
MenuTitle,
SectionHeader,
} from '@woocommerce/components';
import { useUserPreferences } from '@woocommerce/data';
import { useUserPreferences, ITEMS_STORE_NAME } from '@woocommerce/data';
import { getSetting } from '@woocommerce/wc-admin-settings';
import { recordEvent } from '@woocommerce/tracks';
@ -21,7 +21,6 @@ import { recordEvent } from '@woocommerce/tracks';
* Internal dependencies
*/
import Leaderboard from '../../analytics/components/leaderboard';
import withSelect from '../../wc-api/with-select';
import './style.scss';
const renderLeaderboardToggles = ( {
@ -179,9 +178,7 @@ Leaderboards.propTypes = {
export default compose(
withSelect( ( select ) => {
const { getItems, getItemsError, isGetItemsRequesting } = select(
'wc-api'
);
const { getItems, getItemsError } = select( ITEMS_STORE_NAME );
const { leaderboards: allLeaderboards } = getSetting( 'dataEndpoints', {
leaderboards: [],
} );
@ -190,7 +187,6 @@ export default compose(
allLeaderboards,
getItems,
getItemsError,
isGetItemsRequesting,
};
} )
)( Leaderboards );

View File

@ -5,6 +5,7 @@ import { __, _n, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import Gridicon from 'gridicons';
import PropTypes from 'prop-types';
import interpolateComponents from 'interpolate-components';
@ -19,7 +20,12 @@ import {
} from '@woocommerce/components';
import { getNewPath } from '@woocommerce/navigation';
import { getAdminLink, getSetting } from '@woocommerce/wc-admin-settings';
import { SETTINGS_STORE_NAME, REPORTS_STORE_NAME } from '@woocommerce/data';
import {
SETTINGS_STORE_NAME,
REPORTS_STORE_NAME,
ITEMS_STORE_NAME,
QUERY_DEFAULTS,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
/**
@ -28,9 +34,7 @@ import { recordEvent } from '@woocommerce/tracks';
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header';
import ActivityOutboundLink from '../activity-outbound-link';
import { QUERY_DEFAULTS } from '../../../wc-api/constants';
import { DEFAULT_ACTIONABLE_STATUSES } from '../../../analytics/settings/config';
import withSelect from '../../../wc-api/with-select';
import { CurrencyContext } from '../../../lib/currency-context';
class OrdersPanel extends Component {
@ -330,12 +334,9 @@ OrdersPanel.contextType = CurrencyContext;
export default compose(
withSelect( ( select, props ) => {
const { hasActionableOrders } = props;
const {
getItems,
getItemsError,
getItemsTotalCount,
isGetItemsRequesting,
} = select( 'wc-api' );
const { getItems, getItemsError, getItemsTotalCount } = select(
ITEMS_STORE_NAME
);
const { getReportItems, getReportItemsError, isResolving } = select(
REPORTS_STORE_NAME
);
@ -363,10 +364,10 @@ export default compose(
const actionableOrders = Array.from(
getItems( 'orders', actionableOrdersQuery ).values()
);
const isRequestingActionable = isGetItemsRequesting(
const isRequestingActionable = isResolving( 'getItems', [
'orders',
actionableOrdersQuery
);
actionableOrdersQuery,
] );
if ( isRequestingActionable ) {
return {
@ -434,7 +435,10 @@ export default compose(
allOrdersQuery
);
const isError = Boolean( getItemsError( 'orders', allOrdersQuery ) );
const isRequesting = isGetItemsRequesting( 'orders', allOrdersQuery );
const isRequesting = isResolving( 'getItems', [
'orders',
allOrdersQuery,
] );
return {
hasNonActionableOrders: totalNonActionableOrders > 0,

View File

@ -9,6 +9,7 @@ import { compose } from '@wordpress/compose';
import { ESCAPE } from '@wordpress/keycodes';
import { get } from 'lodash';
import { withDispatch } from '@wordpress/data';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
import { Link, ProductImage } from '@woocommerce/components';
import { getSetting } from '@woocommerce/wc-admin-settings';
import { recordEvent } from '@woocommerce/tracks';
@ -75,13 +76,32 @@ class ProductStockCard extends Component {
this.setState( { quantity: event.target.value } );
}
onSubmit() {
const { product, updateProductStock } = this.props;
async onSubmit() {
const { product, updateProductStock, createNotice } = this.props;
const { quantity } = this.state;
this.setState( { editing: false, edited: true } );
updateProductStock( product, quantity );
const results = await updateProductStock( product, quantity );
if ( results.success ) {
createNotice(
'success',
sprintf(
__( '%s stock updated.', 'woocommerce-admin' ),
product.name
)
);
} else {
createNotice(
'error',
sprintf(
__( '%s stock could not be updated.', 'woocommerce-admin' ),
product.name
)
);
}
this.recordStockEvent( 'save', {
quantity,
} );
@ -226,10 +246,12 @@ class ProductStockCard extends Component {
export default compose(
withDispatch( ( dispatch ) => {
const { updateProductStock } = dispatch( 'wc-api' );
const { createNotice } = dispatch( 'core/notices' );
const { updateProductStock } = dispatch( ITEMS_STORE_NAME );
return {
updateProductStock,
createNotice,
};
} )
)( ProductStockCard );

View File

@ -4,9 +4,11 @@
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import PropTypes from 'prop-types';
import Gridicon from 'gridicons';
import { EmptyContent, Section } from '@woocommerce/components';
import { QUERY_DEFAULTS, ITEMS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
@ -14,8 +16,6 @@ import { EmptyContent, Section } from '@woocommerce/components';
import { ActivityCard, ActivityCardPlaceholder } from '../../activity-card';
import ActivityHeader from '../../activity-header';
import ProductStockCard from './card';
import { QUERY_DEFAULTS } from '../../../../wc-api/constants';
import withSelect from '../../../../wc-api/with-select';
class StockPanel extends Component {
renderEmptyCard() {
@ -111,8 +111,8 @@ StockPanel.defaultProps = {
export default compose(
withSelect( ( select ) => {
const { getItems, getItemsError, isGetItemsRequesting } = select(
'wc-api'
const { getItems, getItemsError, isResolving } = select(
ITEMS_STORE_NAME
);
const productsQuery = {
@ -126,7 +126,10 @@ export default compose(
getItems( 'products', productsQuery ).values()
);
const isError = Boolean( getItemsError( 'products', productsQuery ) );
const isRequesting = isGetItemsRequesting( 'products', productsQuery );
const isRequesting = isResolving( 'getItems', [
'products',
productsQuery,
] );
return { products, isError, isRequesting };
} )

View File

@ -6,6 +6,8 @@ import {
REVIEWS_STORE_NAME,
SETTINGS_STORE_NAME,
USER_STORE_NAME,
ITEMS_STORE_NAME,
QUERY_DEFAULTS,
} from '@woocommerce/data';
import { getSetting } from '@woocommerce/wc-admin-settings';
@ -13,7 +15,6 @@ import { getSetting } from '@woocommerce/wc-admin-settings';
* Internal dependencies
*/
import { DEFAULT_ACTIONABLE_STATUSES } from '../../analytics/settings/config';
import { QUERY_DEFAULTS } from '../../wc-api/constants';
import { getUnreadNotesCount } from './panels/inbox/utils';
export function getUnreadNotes( select ) {
@ -60,12 +61,9 @@ export function getUnreadNotes( select ) {
}
export function getUnreadOrders( select ) {
const {
getItems,
getItemsTotalCount,
getItemsError,
isGetItemsRequesting,
} = select( 'wc-api' );
const { getItems, getItemsTotalCount, getItemsError, isResolving } = select(
ITEMS_STORE_NAME
);
const { getSetting: getMutableSetting } = select( SETTINGS_STORE_NAME );
const {
woocommerce_actionable_order_statuses: orderStatuses = DEFAULT_ACTIONABLE_STATUSES,
@ -89,7 +87,7 @@ export function getUnreadOrders( select ) {
// eslint-disable-next-line @wordpress/no-unused-vars-before-return
const totalOrders = getItemsTotalCount( 'orders', ordersQuery );
const isError = Boolean( getItemsError( 'orders', ordersQuery ) );
const isRequesting = isGetItemsRequesting( 'orders', ordersQuery );
const isRequesting = isResolving( 'getItems', [ 'orders', ordersQuery ] );
if ( isError || isRequesting ) {
return null;

View File

@ -1,12 +0,0 @@
/**
* Internal dependencies
*/
import mutations from './mutations';
import operations from './operations';
import selectors from './selectors';
export default {
mutations,
operations,
selectors,
};

View File

@ -1,60 +0,0 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
const updateProductStock = ( operations ) => async ( product, newStock ) => {
const { createNotice } = dispatch( 'core/notices' );
const oldStockQuantity = product.stock_quantity;
const resourceName = getResourceName(
'items-query-products-item',
product.id
);
// Optimistically update product stock
operations.updateLocally( [ resourceName ], {
[ resourceName ]: { ...product, stock_quantity: newStock },
} );
const result = await operations.update( [ resourceName ], {
[ resourceName ]: {
id: product.id,
type: product.type,
parent_id: product.parent_id,
stock_quantity: newStock,
},
} );
const response = result[ 0 ][ resourceName ];
if ( response && response.data ) {
createNotice(
'success',
sprintf(
__( '%s stock updated.', 'woocommerce-admin' ),
product.name
)
);
}
if ( response && response.error ) {
createNotice(
'error',
sprintf(
__( '%s stock could not be updated.', 'woocommerce-admin' ),
product.name
)
);
// Revert local changes if the operation failed in the server
operations.updateLocally( [ resourceName ], {
[ resourceName ]: { ...product, stock_quantity: oldStockQuantity },
} );
}
};
export default {
updateProductStock,
};

View File

@ -1,149 +0,0 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import {
getResourceIdentifier,
getResourcePrefix,
getResourceName,
} from '../utils';
import { NAMESPACE } from '../constants';
const typeEndpointMap = {
'items-query-categories': 'products/categories',
'items-query-customers': 'customers',
'items-query-coupons': 'coupons',
'items-query-leaderboards': 'leaderboards',
'items-query-orders': 'orders',
'items-query-products': 'products',
'items-query-taxes': 'taxes',
};
function read( resourceNames, fetch = apiFetch ) {
const filteredNames = resourceNames.filter( ( name ) => {
const prefix = getResourcePrefix( name );
return Boolean( typeEndpointMap[ prefix ] );
} );
return filteredNames.map( async ( resourceName ) => {
const prefix = getResourcePrefix( resourceName );
const endpoint = typeEndpointMap[ prefix ];
const query = getResourceIdentifier( resourceName );
const url = addQueryArgs( `${ NAMESPACE }/${ endpoint }`, query );
const isUnboundedRequest = query.per_page === -1;
try {
const response = await fetch( {
/* eslint-disable max-len */
/**
* A false parse flag allows a full response including headers which are useful
* to determine totalCount. However, this invalidates an unbounded request, ie
* `per_page: -1` by skipping middleware in apiFetch.
*
* See the Gutenberg code for more:
* https://github.com/WordPress/gutenberg/blob/dee3dcf49028717b4af3164e3096bfe747c41ed2/packages/api-fetch/src/middlewares/fetch-all-middleware.js#L39-L45
*/
/* eslint-enable max-len */
parse: isUnboundedRequest,
path: url,
} );
let items;
let totalCount;
if ( isUnboundedRequest ) {
items = response;
totalCount = items.length;
} else {
items = await response.json();
totalCount = parseInt(
response.headers.get( 'x-wp-total' ),
10
);
}
const ids = items.map( ( item ) => item.id );
const itemResources = items.reduce( ( resources, item ) => {
resources[ getResourceName( `${ prefix }-item`, item.id ) ] = {
data: item,
};
return resources;
}, {} );
return {
[ resourceName ]: {
data: ids,
totalCount,
},
...itemResources,
};
} catch ( error ) {
return { [ resourceName ]: { error } };
}
} );
}
function update( resourceNames, data, fetch = apiFetch ) {
const updateableTypes = [ 'items-query-products-item' ];
const filteredNames = resourceNames.filter( ( name ) => {
return updateableTypes.includes( getResourcePrefix( name ) );
} );
return filteredNames.map( async ( resourceName ) => {
const { id, parent_id: parentId, type, ...itemData } = data[
resourceName
];
let url = NAMESPACE;
switch ( type ) {
case 'variation':
url += `/products/${ parentId }/variations/${ id }`;
break;
case 'variable':
case 'simple':
default:
url += `/products/${ id }`;
}
return fetch( { path: url, method: 'PUT', data: itemData } )
.then( ( item ) => {
return { [ resourceName ]: { data: item } };
} )
.catch( ( error ) => {
return { [ resourceName ]: { error } };
} );
} );
}
function updateLocally( resourceNames, data ) {
const updateableTypes = [ 'items-query-products-item' ];
const filteredNames = resourceNames.filter( ( name ) => {
return updateableTypes.includes( getResourcePrefix( name ) );
} );
const lowStockResourceName = getResourceName( 'items-query-products', {
page: 1,
per_page: 1,
low_in_stock: true,
status: 'publish',
} );
return filteredNames.map( async ( resourceName ) => {
return {
[ resourceName ]: { data: data[ resourceName ] },
// Force low stock products to be re-fetched after updating an item.
[ lowStockResourceName ]: { lastReceived: null },
};
} );
}
export default {
read,
update,
updateLocally,
};

View File

@ -1,56 +0,0 @@
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';
const getItems = ( getResource, requireResource ) => (
type,
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
const ids = requireResource( requirement, resourceName ).data || [];
const items = new Map();
ids.forEach( ( id ) => {
items.set(
id,
getResource( getResourceName( `items-query-${ type }-item`, id ) )
.data
);
} );
return items;
};
const getItemsTotalCount = ( getResource ) => ( type, query = {} ) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
return getResource( resourceName ).totalCount || 0;
};
const getItemsError = ( getResource ) => ( type, query = {} ) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
return getResource( resourceName ).error;
};
const isGetItemsRequesting = ( getResource ) => ( type, query = {} ) => {
const resourceName = getResourceName( `items-query-${ type }`, query );
const { lastRequested, lastReceived } = getResource( resourceName );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getItems,
getItemsError,
getItemsTotalCount,
isGetItemsRequesting,
};

View File

@ -1,18 +1,14 @@
/**
* Internal dependencies
*/
import items from './items';
import imports from './imports';
function createWcApiSpec() {
return {
name: 'wcApi',
mutations: {
...items.mutations,
},
mutations: {},
selectors: {
...imports.selectors,
...items.selectors,
},
operations: {
read( resourceNames ) {
@ -21,18 +17,13 @@ function createWcApiSpec() {
return [];
}
return [
...imports.operations.read( resourceNames ),
...items.operations.read( resourceNames ),
];
return [ ...imports.operations.read( resourceNames ) ];
},
update( resourceNames, data ) {
return [ ...items.operations.update( resourceNames, data ) ];
update() {
return [];
},
updateLocally( resourceNames, data ) {
return [
...items.operations.updateLocally( resourceNames, data ),
];
updateLocally() {
return [];
},
},
};

View File

@ -21,6 +21,10 @@ export { REVIEWS_STORE_NAME } from './reviews';
export { NOTES_STORE_NAME } from './notes';
export { REPORTS_STORE_NAME } from './reports';
export { ITEMS_STORE_NAME } from './items';
export { getLeaderboard, searchItemsByString } from './items/utils';
export {
getFilterQuery,
getSummaryNumbers,

View File

@ -0,0 +1,6 @@
const TYPES = {
SET_ITEMS: 'SET_ITEMS',
SET_ERROR: 'SET_ERROR',
};
export default TYPES;

View File

@ -0,0 +1,63 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { NAMESPACE } from '../constants';
export function setItems( itemType, query, items, totalCount ) {
return {
type: TYPES.SET_ITEMS,
items,
itemType,
query,
totalCount,
};
}
export function setError( itemType, query, error ) {
return {
type: TYPES.SET_ERROR,
itemType,
query,
error,
};
}
export function* updateProductStock( product, quantity ) {
const updatedProduct = { ...product, stock_quantity: quantity };
const { id, parent_id: parentId, type } = updatedProduct;
// Optimistically update product stock.
yield setItems( 'products', id, [ updatedProduct ], 1 );
let url = NAMESPACE;
switch ( type ) {
case 'variation':
url += `/products/${ parentId }/variations/${ id }`;
break;
case 'variable':
case 'simple':
default:
url += `/products/${ id }`;
}
try {
const results = yield apiFetch( {
path: url,
method: 'PUT',
data: updatedProduct,
} );
return { success: true, ...results };
} catch ( error ) {
// Update failed, return product back to original state.
yield setItems( 'products', id, [ product ], 1 );
yield setError( id, error );
return { success: false, ...error };
}
}

View File

@ -0,0 +1 @@
export const STORE_NAME = 'wc/admin/items';

View File

@ -0,0 +1,32 @@
/**
* External dependencies
*/
import { registerStore, select } 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';
const storeSelectors = select( STORE_NAME );
// @todo This is used to prevent double registration of the store due to webpack chunks.
// The `storeSelectors` condition can be removed once this is fixed.
// See https://github.com/woocommerce/woocommerce-admin/issues/4443.
if ( ! storeSelectors ) {
registerStore( STORE_NAME, {
reducer,
actions,
controls,
selectors,
resolvers,
} );
}
export const ITEMS_STORE_NAME = STORE_NAME;

View File

@ -0,0 +1,51 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { getResourceName } from '../utils';
const reducer = (
state = {
items: {},
errors: {},
data: {},
},
{ type, itemType, query, items, totalCount, error }
) => {
switch ( type ) {
case TYPES.SET_ITEMS:
const ids = [];
const nextItems = items.reduce( ( result, item ) => {
ids.push( item.id );
result[ item.id ] = item;
return result;
}, {} );
const resourceName = getResourceName( itemType, query );
return {
...state,
items: {
...state.items,
[ resourceName ]: { data: ids, totalCount },
},
data: {
...state.data,
[ itemType ]: {
...state.data[ itemType ],
...nextItems,
},
},
};
case TYPES.SET_ERROR:
return {
...state,
errors: {
...state.errors,
[ getResourceName( itemType, query ) ]: error,
},
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { NAMESPACE } from '../constants';
import { setError, setItems } from './actions';
import { fetchWithHeaders } from '../controls';
export function* getItems( itemType, query ) {
const endpoint =
itemType === 'categories' ? 'products/categories' : itemType;
try {
const url = addQueryArgs( `${ NAMESPACE }/${ endpoint }`, query );
const isUnboundedRequest = query.per_page === -1;
const fetch = isUnboundedRequest ? apiFetch : fetchWithHeaders;
const response = yield fetch( {
path: url,
method: 'GET',
} );
if ( isUnboundedRequest ) {
yield setItems( itemType, query, response, response.length );
} else {
const totalCount = parseInt(
response.headers.get( 'x-wp-total' ),
10
);
yield setItems( itemType, query, response.data, totalCount );
}
} catch ( error ) {
yield setError( query, error );
}
}
export function* getReviewsTotalCount( itemType, query ) {
yield getItems( itemType, query );
}

View File

@ -0,0 +1,29 @@
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
export const getItems = ( state, itemType, query ) => {
const resourceName = getResourceName( itemType, query );
const ids =
( state.items[ resourceName ] && state.items[ resourceName ].data ) ||
[];
return ids.reduce( ( map, id ) => {
map.set( id, state.data[ itemType ][ id ] );
return map;
}, new Map() );
};
export const getItemsTotalCount = ( state, itemType, query ) => {
const resourceName = getResourceName( itemType, query );
return (
( state.items[ resourceName ] &&
state.items[ resourceName ].totalCount ) ||
0
);
};
export const getItemsError = ( state, itemType, query ) => {
const resourceName = getResourceName( itemType, query );
return state.errors[ resourceName ];
};

View File

@ -0,0 +1,62 @@
/**
* Internal dependencies
*/
import reducer from '../reducer';
import TYPES from '../action-types';
import { getResourceName } from '../../utils';
const defaultState = {
items: {},
errors: {},
data: {},
};
describe( 'items reducer', () => {
it( 'should return a default state', () => {
const state = reducer( undefined, {} );
expect( state ).toEqual( defaultState );
expect( state ).not.toBe( defaultState );
} );
it( 'should handle SET_ITEMS', () => {
const items = [
{ id: 1, title: 'Yum!' },
{ id: 2, title: 'Dynamite!' },
];
const totalCount = 45;
const query = { status: 'flavortown' };
const itemType = 'BBQ';
const state = reducer( defaultState, {
type: TYPES.SET_ITEMS,
items,
itemType,
query,
totalCount,
} );
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 ].totalCount ).toBe( 45 );
expect( state.data[ itemType ][ '1' ] ).toBe( items[ 0 ] );
expect( state.data[ itemType ][ '2' ] ).toBe( items[ 1 ] );
} );
it( 'should handle SET_ERROR', () => {
const query = { status: 'flavortown' };
const itemType = 'BBQ';
const resourceName = getResourceName( itemType, query );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.SET_ERROR,
itemType,
query,
error,
} );
expect( state.errors[ resourceName ] ).toBe( error );
} );
} );

View File

@ -3,6 +3,11 @@
*/
import { appendTimestamp, getCurrentDates } from '@woocommerce/date';
/**
* Internal dependencies
*/
import { STORE_NAME } from './constants';
/**
* Returns leaderboard data to render a leaderboard table.
*
@ -24,9 +29,7 @@ export function getLeaderboard( options ) {
select,
filterQuery,
} = options;
const { getItems, getItemsError, isGetItemsRequesting } = select(
'wc-api'
);
const { getItems, getItemsError, isResolving } = select( STORE_NAME );
const response = {
isRequesting: false,
isError: false,
@ -47,7 +50,7 @@ export function getLeaderboard( options ) {
// eslint-disable-next-line @wordpress/no-unused-vars-before-return
const leaderboards = getItems( endpoint, leaderboardQuery );
if ( isGetItemsRequesting( endpoint, leaderboardQuery ) ) {
if ( isResolving( 'getItems', [ endpoint, leaderboardQuery ] ) ) {
return { ...response, isRequesting: true };
} else if ( getItemsError( endpoint, leaderboardQuery ) ) {
return { ...response, isError: true };
@ -66,9 +69,7 @@ export function getLeaderboard( options ) {
* @return {Object} Object containing API request information and the matching items.
*/
export function searchItemsByString( select, endpoint, search ) {
const { getItems, getItemsError, isGetItemsRequesting } = select(
'wc-api'
);
const { getItems, getItemsError, isResolving } = select( STORE_NAME );
const items = {};
let isRequesting = false;
@ -82,7 +83,7 @@ export function searchItemsByString( select, endpoint, search ) {
newItems.forEach( ( item, id ) => {
items[ id ] = item;
} );
if ( isGetItemsRequesting( endpoint, query ) ) {
if ( isResolving( 'getItems', [ endpoint, query ] ) ) {
isRequesting = true;
}
if ( getItemsError( endpoint, query ) ) {