--- sites product-categories actions ---
/** @format */

/**
 * Internal dependencies
 */
import { getNormalizedProductCategoriesQuery } from './utils';
import {
        WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST,
        WOOCOMMERCE_PRODUCT_CATEGORY_CREATE,
        WOOCOMMERCE_PRODUCT_CATEGORY_UPDATE,
        WOOCOMMERCE_PRODUCT_CATEGORY_UPDATED,
        WOOCOMMERCE_PRODUCT_CATEGORY_DELETE,
} from 'woocommerce/state/action-types';

export function fetchProductCategories( siteId, query = {} ) {
        const normalizedQuery = getNormalizedProductCategoriesQuery( query );
        return dispatch => {
                const getAction = {
                        type: WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST,
                        siteId,
                        query: normalizedQuery,
                };

                dispatch( getAction );
        };
}

/**
 * Action Creator: Create a new product category.
 *
 * @param {Number} siteId The id of the site upon which to create.
 * @param {Object} category The product category object (may include a placeholder id).
 * @param {Object|Function} [successAction] action with extra props { sentData, receivedData }
 * @param {Object|Function} [failureAction] action with extra props { error }
 * @return {Object} Action object
 */
export function createProductCategory( siteId, category, successAction, failureAction ) {
        // TODO: Error action if not valid?

        const action = {
                type: WOOCOMMERCE_PRODUCT_CATEGORY_CREATE,
                siteId,
                category,
                successAction,
                failureAction,
        };

        return action;
}

/**
 * Action Creator: Update a product category.
 *
 * @param {Number} siteId The id of the site upon which to create.
 * @param {Object} category The product category object.
 * @param {Object|Function} [successAction] action with extra props { sentData, receivedData }
 * @param {Object|Function} [failureAction] action with extra props { error }
 * @return {Object} Action object
 */
export function updateProductCategory( siteId, category, successAction, failureAction ) {
        const action = {
                type: WOOCOMMERCE_PRODUCT_CATEGORY_UPDATE,
                siteId,
                category,
                successAction,
                failureAction,
        };

        return action;
}

/**
 * Action Creator: Delete a product category.
 *
 * @param {Number} siteId The id of the site upon which to delete.
 * @param {Object} category The product category object.
 * @param {Object|Function} [successAction] action with extra props { sentData, receivedData }
 * @param {Object|Function} [failureAction] action with extra props { error }
 * @return {Object} Action object
 */
export function deleteProductCategory( siteId, category, successAction, failureAction ) {
        return {
                type: WOOCOMMERCE_PRODUCT_CATEGORY_DELETE,
                siteId,
                category,
                successAction,
                failureAction,
        };
}

/**
 * Action Creator: This action prompts the state to update itself after a product category has changed.
 *
 * @param {Number} siteId The id of the site to which the category belongs.
 * @param {Object} data The complete product category object with which to update the state.
 * @param {Object} originatingAction The action that precipitated this update.
 * @return {Object} Action object
 */
export function productCategoryUpdated( siteId, data, originatingAction ) {
        return {
                type: WOOCOMMERCE_PRODUCT_CATEGORY_UPDATED,
                siteId,
                data,
                originatingAction,
        };
}
--- sites product-categories reducer ---
/** @format */

/**
 * External dependencies
 */

import { keyBy, omit, reject } from 'lodash';

/**
 * Internal dependencies
 */
import { combineReducers } from 'state/utils';
import { getSerializedProductCategoriesQuery } from './utils';
import {
        WOOCOMMERCE_PRODUCT_CATEGORY_UPDATED,
        WOOCOMMERCE_PRODUCT_CATEGORY_DELETED,
        WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST,
        WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_SUCCESS,
        WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_FAILURE,
} from 'woocommerce/state/action-types';

/**
 * Returns if a product categories request for a specific query is in progress or not.
 *
 * @param  {Object} state  Current state
 * @param  {Object} action Action payload
 * @return {Object}        Updated state
 */
export function isQueryLoading( state = {}, action ) {
        switch ( action.type ) {
                case WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST:
                case WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_SUCCESS:
                case WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_FAILURE:
                        const query = getSerializedProductCategoriesQuery( action.query );
                        return Object.assign( {}, state, {
                                [ query ]: WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST === action.type,
                        } );
                default:
                        return state;
        }
}

/**
 * Returns if a product categories request for a specific query has returned an error.
 *
 * @param  {Object} state  Current state
 * @param  {Object} action Action payload
 * @return {Object}        Updated state
 */
export function isQueryError( state = {}, action ) {
        if ( WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_FAILURE === action.type && action.error ) {
                const query = getSerializedProductCategoriesQuery( action.query );
                return Object.assign( {}, state, { [ query ]: true } );
        }

        return state;
}

/**
 * Tracks all known categories objects, indexed by ID.
 *
 * @param  {Object} state  Current state
 * @param  {Object} action Action payload
 * @return {Object}        Updated state
 */
export function items( state = {}, action ) {
        if ( WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_SUCCESS === action.type && action.data ) {
                const cats = keyBy( action.data, 'id' );
                return Object.assign( {}, state, cats );
        }

        if ( WOOCOMMERCE_PRODUCT_CATEGORY_UPDATED === action.type && action.data ) {
                return {
                        ...state,
                        [ action.data.id ]: action.data,
                };
        }

        if ( WOOCOMMERCE_PRODUCT_CATEGORY_DELETED === action.type && action.category ) {
                return keyBy( reject( state, { id: action.category.id } ), 'id' );
        }

        return state;
}

/**
 * Tracks the categories which belong to a query, as a list of IDs
 * referencing items in `productCategories.items`.
 *
 * @param  {Object} state  Current state
 * @param  {Object} action Action payload
 * @return {Object}        Updated state
 */
export function queries( state = {}, action ) {
        if ( WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_SUCCESS === action.type && action.data ) {
                const idList = action.data.map( cat => cat.id );
                const query = getSerializedProductCategoriesQuery( action.query );
                return Object.assign( {}, state, { [ query ]: idList } );
        }

        return state;
}

/**
 * Tracks the total number of product categories for the current query.
 *
 * @param  {Object} state  Current state
 * @param  {Object} action Action payload
 * @return {Object}        Updated state
 */
export function total( state = {}, action ) {
        if ( WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_SUCCESS === action.type && action.data ) {
                const query = getSerializedProductCategoriesQuery( omit( action.query, [ 'page', 'offset' ] ) );
                return Object.assign( {}, state, { [ query ]: action.total } );
        }

        return state;
}

/**
 * Tracks the total number of pages for the current query.
 *
 * @param  {Object} state  Current state
 * @param  {Object} action Action payload
 * @return {Object}        Updated state
 */
export function totalPages( state = {}, action ) {
        if ( WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_SUCCESS === action.type && action.data ) {
                const query = getSerializedProductCategoriesQuery( omit( action.query, [ 'page', 'offset' ] ) );
                return Object.assign( {}, state, { [ query ]: action.totalPages } );
        }

        return state;
}

export default combineReducers( {
        isQueryLoading,
        isQueryError,
        items,
        queries,
        total,
        totalPages,
} );
--- sites product-categories selectors ---
/** @format */
/**
 * External dependencies
 */
import { get, isArray, omit, some, range, values } from 'lodash';

/**
 * Internal dependencies
 */
import { getSelectedSiteId } from 'state/ui/selectors';
import { getSerializedProductCategoriesQuery } from './utils';

/**
 * @param {Object} state Whole Redux state tree
 * @param {Number} [siteId] Site ID to check. If not provided, the Site ID selected in the UI will be used
 * @return {Object} State tree local to product categories reducer
 */
function getRawCategoryState( state, siteId = getSelectedSiteId( state ) ) {
        return get( state, [ 'extensions', 'woocommerce', 'sites', siteId, 'productCategories' ], {} );
}

/**
 * @param {Object} state Whole Redux state tree
 * @param {Object} [query] Query used to fetch product categories. If not provided, API defaults are used.
 * @param {Number} [siteId] Site ID to check. If not provided, the Site ID selected in the UI will be used
 * @return {boolean} Whether product categories have been successfully loaded from the server
 */
export function areProductCategoriesLoaded(
        state,
        query = {},
        siteId = getSelectedSiteId( state )
) {
        const serializedQuery = getSerializedProductCategoriesQuery( query );
        const categoryState = getRawCategoryState( state, siteId );
        const cats = categoryState.queries && categoryState.queries[ serializedQuery ];

        return isArray( cats );
}

/**
 * @param {Object} state Whole Redux state tree
 * @param {Object} [query] Query used to fetch product categories. If not provided, API defaults are used.
 * @param {Number} [siteId] Site ID to check. If not provided, the Site ID selected in the UI will be used
 * @return {boolean} Whether product categories are currently being retrieved from the server
 */
export function areProductCategoriesLoading(
        state,
        query = {},
        siteId = getSelectedSiteId( state )
) {
        const serializedQuery = getSerializedProductCategoriesQuery( query );
        const categoryState = getRawCategoryState( state, siteId );
        const isLoading = categoryState.isQueryLoading && categoryState.isQueryLoading[ serializedQuery ];
        // Strict check because it could also be undefined.
        return true === isLoading;
}

/**
 * Returns true if currently requesting product categories for any query, or false otherwise.
 *
 * @param {Object} state Whole Redux state tree
 * @param {Number} [siteId] Site ID to check. If not provided, the Site ID selected in the UI will be used
 * @return {Boolean}       Returns true if currently requesting product categories for any query
 */
export function areAnyProductCategoriesLoading( state, siteId = getSelectedSiteId( state ) ) {
        const categoryState = getRawCategoryState( state, siteId );
        return some( categoryState.isQueryLoading );
}

/**
 * Gets product category fetched from API data.
 *
 * @param {Object} state Global state tree
 * @param {Number} categoryId The id of the category sought
 * @param {Number} siteId wpcom site id
 * @return {Object|null} Product category if found, otherwise null.
 */
export function getProductCategory( state, categoryId, siteId = getSelectedSiteId( state ) ) {
        const categoryState = getRawCategoryState( state, siteId );
        const category = categoryState.items && categoryState.items[ categoryId ];
        if ( ! category ) {
                return null;
        }
        const label = getProductCategoryLabel( state, categoryId, siteId );
        return { ...category, label };
}

/**
 * Gets product categories from API data.
 *
 * @param {Object} state Global state tree
 * @param {Object} [query] Query used to fetch product categories. If not provided, API defaults are used.
 * @param {Number} [siteId] wpcom site id, if not provided, uses the selected site id.
 * @return {Array} List of product categories
 */
export function getProductCategories( state, query = {}, siteId = getSelectedSiteId( state ) ) {
        if ( ! areProductCategoriesLoaded( state, query, siteId ) ) {
                return [];
        }
        const serializedQuery = getSerializedProductCategoriesQuery( query );
        const categoryState = getRawCategoryState( state, siteId );
        const idsForQuery = categoryState.queries && categoryState.queries[ serializedQuery ];

        if ( idsForQuery.length ) {
                return idsForQuery.map( id => getProductCategory( state, id, siteId ) );
        }

        return [];
}

/**
 * Gets all product categories from API data, as currently loaded in the state (might not
 * be all the products on the remote site, if they haven't all been requested).
 *
 * @param {Object} state Global state tree
 * @param {Number} [siteId] wpcom site id, if not provided, uses the selected site id.
 * @return {Array} List of product categories
 */
export function getAllProductCategories( state, siteId = getSelectedSiteId( state ) ) {
        const categoryState = getRawCategoryState( state, siteId );
        const items = values( categoryState.items ) || [];
        return items.map( cat => getProductCategory( state, cat.id, siteId ) );
}

/**
 * Gets all product categories from API data, as currently loaded in the state (might not
 * be all the products on the remote site, if they haven't all been requested).
 *
 * @param {Object} state Global state tree
 * @param {String} search Search term to filter responses
 * @param {Number} [siteId] wpcom site id, if not provided, uses the selected site id.
 * @return {Array} List of product categories for a search query
 */
export function getAllProductCategoriesBySearch(
        state,
        search,
        siteId = getSelectedSiteId( state )
) {
        const lastPage = getProductCategoriesLastPage( state, { search }, siteId );
        if ( null === lastPage ) {
                return [];
        }

        const result = [];
        range( 1, lastPage + 1 ).some( page => {
                const query = {
                        search,
                        page,
                };
                const categories = getProductCategories( state, query, siteId );
                result.push( ...categories );
        } );

        return result;
}

/**
 * @param {Object} state Whole Redux state tree
 * @param {Object} [query] Query used to fetch product categories<response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE>     store.dispatch(
                put( siteId, `products/categories/${ id }`, categoryData, updatedAction, failureAction )
        );
}

export function handleProductCategoryDelete( store, action ) {
        const { siteId, category, successAction, failureAction } = action;

        // Filter out any id we might have.
        const { id, ...categoryData } = category;

        if ( 'number' !== typeof id ) {
                store.dispatch(
                        setError( siteId, action, {
                                message: 'Attempting to delete product category which has a placeholder ID.',
                                category,
                        } )
                );
                return;
        }

        store.dispatch(
                del(
                        siteId,
                        `products/categories/${ id }?force=true`,
                        categoryData,
                        successAction,
                        failureAction
                )
        );
}

export function handleProductCategoryDeleteSuccess( { dispatch }, action, category ) {
        const { siteId } = action;
        dispatch( {
                type: WOOCOMMERCE_PRODUCT_CATEGORY_DELETED,
                siteId,
                category,
        } );
}

export function handleProductCategoriesRequest( { dispatch }, action ) {
        const { siteId, query } = action;
        const requestQuery = { ...DEFAULT_QUERY, ...query };
        const queryString = stringify( omitBy( requestQuery, val => '' === val ) );

        dispatch( request( siteId, action ).getWithHeaders( `products/categories?${ queryString }` ) );
}

export function handleProductCategoriesSuccess( { dispatch }, action, { data } ) {
        const { siteId, query } = action;
        const { headers, body, status } = data;

        // handleProductCategoriesRequest uses &_envelope
        // ( https://developer.wordpress.org/rest-api/using-the-rest-api/global-parameters/#_envelope )
        // so that we can get the X-WP-Total header back from the end-site. This means we will always get a 200
        // status back, and the real status of the request will be stored in the response. This checks the real status.
        if ( status !== 200 || ! isValidCategoriesArray( body ) ) {
                dispatch( {
                        type: WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_FAILURE,
                        siteId,
                        error: 'Invalid Categories Array',
                        query,
                } );
                dispatch( setError( siteId, action, { message: 'Invalid Categories Array', body } ) );
                return;
        }

        const total = headers[ 'X-WP-Total' ];
        const totalPages = headers[ 'X-WP-TotalPages' ];

        dispatch( {
                type: WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_SUCCESS,
                siteId,
                data: body,
                total,
                totalPages,
                query,
        } );

        if ( undefined !== query.offset ) {
                const remainder = total - query.offset - body.length;
                if ( remainder ) {
                        const offset = query.offset + body.length;
                        dispatch( fetchProductCategories( siteId, { ...query, offset } ) );
                }
        }
}

export function handleProductCategoriesError( { dispatch }, action, error ) {
        const { siteId, query } = action;

        dispatch( {
                type: WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST_FAILURE,
                siteId,
                error,
                query,
        } );

        dispatch( setError( siteId, action, error ) );
}

function isValidCategoriesArray( categories ) {
        for ( let i = 0; i < categories.length; i++ ) {
                if ( ! isValidProductCategory( categories[ i ] ) ) {
                        // Short-circuit the loop and return now.
                        return false;
                }
        }
        return true;
}

function isValidProductCategory( category ) {
        return (
                category &&
                category.id &&
                'number' === typeof category.id &&
                category.name &&
                'string' === typeof category.name &&
                category.slug &&
                'string' === typeof category.slug
        );
}

export default {
        [ WOOCOMMERCE_PRODUCT_CATEGORY_CREATE ]: [ handleProductCategoryCreate ],
        [ WOOCOMMERCE_PRODUCT_CATEGORY_DELETE ]: [
                handleProductCategoryDelete,
                handleProductCategoryDeleteSuccess,
        ],
        [ WOOCOMMERCE_PRODUCT_CATEGORY_UPDATE ]: [ handleProductCategoryUpdate ],
        [ WOOCOMMERCE_PRODUCT_CATEGORIES_REQUEST ]: [
                dispatchRequest(
                        handleProductCategoriesRequest,
                        handleProductCategoriesSuccess,
                        handleProductCategoriesError
                ),
        ],
};
--- ui product-categories reducers/selectors/actions ---
/** @format */

/**
 * External dependencies
 */

import { uniqueId } from 'lodash';

/**
 * Internal dependencies
 */
import {
        WOOCOMMERCE_PRODUCT_CATEGORY_EDIT,
        WOOCOMMERCE_PRODUCT_CATEGORY_EDIT_CLEAR,
} from 'woocommerce/state/action-types';

/**
 * Generates a new product category placeholder ID
 * This is used for new creates.
 * @return {Object} A new unique placeholder ID.
 */
export function generateProductCategoryId() {
        return { placeholder: uniqueId( 'productCategory_' ) };
}

/**
 * Action creator: Edit a product category
 *
 * @param {Number} siteId The id of the site to which the category belongs.
 * @param {Object} [category] The most recent version of the category object, or null if new.
 * @param {Object} data An object containing the properties to be edited for the object, if null, the category will be removed.
 * @return {Object} The action object.
 */
export function editProductCategory( siteId, category, data ) {
        return {
                type: WOOCOMMERCE_PRODUCT_CATEGORY_EDIT,
                siteId,
                category: category || { id: generateProductCategoryId() },
                data,
        };
}

/**
 * Action Creator: Clear All Product Category Edits
 *
 * @param {Number} siteId The site for which to clear all product category edits.
 * @return {Object} action
 */
export function clearProductCategoryEdits( siteId ) {
        return {
                type: WOOCOMMERCE_PRODUCT_CATEGORY_EDIT_CLEAR,
                siteId,
        };
}
/** @format */

/**
 * External dependencies
 */

import { compact, isEqual } from 'lodash';

/**
 * Internal dependencies
 */
import { createReducer } from 'state/utils';
import {
        WOOCOMMERCE_PRODUCT_CATEGORY_CREATE,
        WOOCOMMERCE_PRODUCT_CATEGORY_UPDATE,
        WOOCOMMERCE_PRODUCT_CATEGORY_EDIT,
        WOOCOMMERCE_PRODUCT_CATEGORY_EDIT_CLEAR,
        WOOCOMMERCE_PRODUCT_CATEGORY_UPDATED,
} from 'woocommerce/state/action-types';
import { getBucket } from '../helpers';

export default createReducer( null, {
        [ WOOCOMMERCE_PRODUCT_CATEGORY_EDIT ]: editProductCategoryAction,
        [ WOOCOMMERCE_PRODUCT_CATEGORY_EDIT_CLEAR ]: clearEditsAction,
        [ WOOCOMMERCE_PRODUCT_CATEGORY_UPDATED ]: productCategoryUpdatedAction,
} );

function productCategoryUpdatedAction( edits, action ) {
        const { originatingAction } = action;

        if ( WOOCOMMERCE_PRODUCT_CATEGORY_CREATE === originatingAction.type ) {
                const prevCategoryId = originatingAction.category.id;
                const prevEdits = edits || {};
                const prevCreates = prevEdits.creates || [];

                const newCreates = compact(
                        prevCreates.map( category => {
                                if ( isEqual( prevCategoryId, category.id ) ) {
                                        // Remove this create, it's no longer needed.
                                        return undefined;
                                }
                                return category;
                        } )
                );

                return {
                        ...prevEdits,
                        creates: newCreates.length ? newCreates : undefined,
                };
        }

        if ( WOOCOMMERCE_PRODUCT_CATEGORY_UPDATE === originatingAction.type ) {
                const prevCategoryId = originatingAction.category.id;
                const prevEdits = edits || {};
                const prevUpdates = prevEdits.updates || [];

                const newUpdates = compact(
                        prevUpdates.map( category => {
                                if ( isEqual( prevCategoryId, category.id ) ) {
                                        return undefined;
                                }
                                return category;
                        } )
                );

                return {
                        ...prevEdits,
                        updates: newUpdates.length ? newUpdates : undefined,
                };
        }
        // TODO: Add support for delete.

        return edits;
}

function clearEditsAction() {
        return null;
}

function editProductCategoryAction( edits, action ) {
        const { category, data } = action;
        const prevEdits = edits || {};
        const bucket = getBucket( category );
        const newArray = editProductCategory( prevEdits[ bucket ], category, data );

        return {
                ...prevEdits,
                [ bucket ]: newArray,
                currentlyEditingId: category.id,
        };
}

function editProductCategory( array, category, data ) {
        const prevArray = array || [];

        let found = false;

        // Look for this object in the appropriate create or edit array first.
        const newArray = compact(
                prevArray.map( c => {
                        if ( isEqual( category.id, c.id ) ) {
                                found = true;

                                // If data is null, remove this edit, otherwise update the edit data.
                                return data ? { ...c, ...data } : undefined;
                        }
                        return c;
                } )
        );

        if ( ! found ) {
                // update or create not already in edit state, so add it now.
                newArray.push( { id: category.id, ...data } );
        }

        return newArray;
}
/** @format */

/**
 * External dependencies
 */

import { get, find, isNumber } from 'lodash';

/**
 * Internal dependencies
 */
import { getSelectedSiteId } from 'state/ui/selectors';
import {
        getProductCategory,
        getAllProductCategories,
} from 'woocommerce/state/sites/product-categories/selectors';
import { getBucket } from '../helpers';

/**
 * Gets all edits for product categories.
 *
 * @param {Object} state Global state tree
 * @param {Number} [siteId] Site ID to check. If not provided, will use selected Site ID
 * @return {Object} All product category edits in the form of { creates: [], updates: [] }
 */
export function getAllProductCategoryEdits( state, siteId = getSelectedSiteId( state ) ) {
        return get(
                state,
                [ 'extensions', 'woocommerce', 'ui', 'productCategories', siteId, 'edits' ],
                {}
        );
}

/**
 * Gets the accumulated edits for a category, if any.
 *
 * @param {Object} state Global state tree
 * @param {Number|Object} categoryId The id of the product category (or { index: # } if pending create)
 * @param {Number} [siteId] Site ID to check. If not provided, will use selected Site ID
 * @return {Object} The current accumulated edits
 */
export function getProductCategoryEdits( state, categoryId, siteId = getSelectedSiteId( state ) ) {
        const edits = getAllProductCategoryEdits( state, siteId );
        const bucket = getBucket( { id: categoryId } );
        const array = get( edits, bucket, [] );

        return find( array, { id: categoryId } );
}

/**
 * Gets a category with local edits overlayed on top of fetched data.
 *
 * @param {Object} state Global state tree
 * @param {Number|Object} categoryId The id of the product category (or { index: # } if pending create)
 * @param {Number} [siteId] Site ID to check. If not provided, will use selected Site ID
 * @return {Object} The category data merged between the fetched data and edits
 */
export function getProductCategoryWithLocalEdits(
        state,
        categoryId,
        siteId = getSelectedSiteId( state )
) {
        const existing = isNumber( categoryId );

        const category = existing && getProductCategory( state, categoryId, siteId );
        const categoryEdits = getProductCategoryEdits( state, categoryId, siteId );

        return category || categoryEdits ? { ...category, ...categoryEdits } : undefined;
}

/**
 * Gets all categories, either fetched, edited, or both.
 *
 * This returns a list of all fetched categories overlaid with updates (if any) and
 * all categories in the creates list as well.
 *
 * @param {Object} state Global state tree
 * @param {Number} [siteId] Site ID to check. If not provided, will use selected Site ID
 * @return {Object} The category list merged between the fetched data, edits, and creates
 */
export function getProductCategoriesWithLocalEdits( state, siteId = getSelectedSiteId( state ) ) {
        const categoryCreates = getAllProductCategoryEdits( state, siteId ).creates || [];
        const fetchedCategories = getAllProductCategories( state, siteId );
        const categoriesWithUpdates = fetchedCategories.map( c =>
                getProductCategoryWithLocalEdits( state, c.id, siteId )
        );

        return [ ...categoryCreates, ...categoriesWithUpdates ];
}

/**
 * Gets the product category last edited in the UI.
 *
 * @param {Object} state Global state tree
 * @param {Number} [siteId] Site ID to check. If not provided, will use selected Site ID
 * @return {Object} The category data merged between the fetched data and edits
 */
export function getCurrentlyEditingProductCategory( state, siteId = getSelectedSiteId( state ) ) {
        const { currentlyEditingId } = getAllProductCategoryEdits( state, siteId );

        return getProductCategoryWithLocalEdits( state, currentlyEditingId, siteId );
}

/**
 * Gets the id of the currently editing product category.
 *
 * @param {Object} state Global state tree
 * @param {Number} [siteId] Site ID to check. If not provided, the Site ID selected in the UI will be used
 * @return {Number|Object} Id of the currently editing product category.
 */
export function getCurrentlyEditingId( state, siteId = getSelectedSiteId( state ) ) {
        const edits = getAllProductCategoryEdits( state, siteId ) || {};
        const { currentlyEditingId } = edits;

        return currentlyEditingId;
}
[The command completed with exit code 0.]
[Current working directory: /workspace/wp-calypso]
[Python interpreter: /usr/bin/python]
[Command finished with exit code 0]