Add/34 create new category field modal (#35132)
* Add initial category field component with new typeahead Move search logic to useCategorySearch hook Add initial add new category logic Add parent category field to add new category modal Adding some debug changes Update category control to make use of internal selectItem function of select control Add changelogs Update pagesize back to 100 Add placeholder Empty placeholder Fix input and icon sizes Fix input underline Add max height and scroll to category dropdown Add sorting of category items Auto open parents when traversing up the tree using arrow keys Add several comments Add some initial unit tests for the category field component Add tests for useCategorySearch hook and fixed minor bug Update styling and autoselect parent if child is selected Fix styling issues for the select control dropdown inside a modal Fix issue with creating new category with parent Add function comment and fixed border styling * Fix up some issues after the rebase * Some small fixes for the Category creation * Fix up some styling issues around the add-new-item * Add changelogs * Remove unneeded export of toggle button props * Fix create category error and tests in attribute field * Fix some minor bugs and styling changes that came up during PR feedback * Fix tests * Make use of $gap variable for css
This commit is contained in:
parent
7bff5cbb6b
commit
1550806efc
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Move classname down in SelectControl Menu so it is on the actual Menu element.
|
|
@ -46,39 +46,41 @@ export const Menu = ( {
|
|||
return (
|
||||
<div
|
||||
ref={ selectControlMenuRef }
|
||||
className={ classnames(
|
||||
'woocommerce-experimental-select-control__menu',
|
||||
className
|
||||
) }
|
||||
className="woocommerce-experimental-select-control__menu"
|
||||
>
|
||||
<Popover
|
||||
// @ts-expect-error this prop does exist, see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L180.
|
||||
__unstableSlotName="woocommerce-select-control-menu"
|
||||
focusOnMount={ false }
|
||||
className={ classnames(
|
||||
'woocommerce-experimental-select-control__popover-menu',
|
||||
{
|
||||
'is-open': isOpen,
|
||||
'has-results': Children.count( children ) > 0,
|
||||
}
|
||||
) }
|
||||
position="bottom center"
|
||||
animate={ false }
|
||||
>
|
||||
<ul
|
||||
{ ...getMenuProps() }
|
||||
className="woocommerce-experimental-select-control__popover-menu-container"
|
||||
style={ {
|
||||
width: boundingRect?.width,
|
||||
} }
|
||||
onMouseUp={ ( e ) =>
|
||||
// Fix to prevent select control dropdown from closing when selecting within the Popover.
|
||||
e.stopPropagation()
|
||||
}
|
||||
<div>
|
||||
<Popover
|
||||
// @ts-expect-error this prop does exist, see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L180.
|
||||
__unstableSlotName="woocommerce-select-control-menu"
|
||||
focusOnMount={ false }
|
||||
className={ classnames(
|
||||
'woocommerce-experimental-select-control__popover-menu',
|
||||
{
|
||||
'is-open': isOpen,
|
||||
'has-results': Children.count( children ) > 0,
|
||||
}
|
||||
) }
|
||||
position="bottom right"
|
||||
animate={ false }
|
||||
>
|
||||
{ isOpen && children }
|
||||
</ul>
|
||||
</Popover>
|
||||
<ul
|
||||
{ ...getMenuProps() }
|
||||
className={ classnames(
|
||||
'woocommerce-experimental-select-control__popover-menu-container',
|
||||
className
|
||||
) }
|
||||
style={ {
|
||||
width: boundingRect?.width,
|
||||
} }
|
||||
onMouseUp={ ( e ) =>
|
||||
// Fix to prevent select control dropdown from closing when selecting within the Popover.
|
||||
e.stopPropagation()
|
||||
}
|
||||
>
|
||||
{ isOpen && children }
|
||||
</ul>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
|
|
|
@ -37,7 +37,7 @@ import {
|
|||
type SelectControlProps< ItemType > = {
|
||||
children?: ChildrenType< ItemType >;
|
||||
items: ItemType[];
|
||||
label: string;
|
||||
label: string | JSX.Element;
|
||||
getItemLabel?: getItemLabelType< ItemType >;
|
||||
getItemValue?: getItemValueType< ItemType >;
|
||||
getFilteredItems?: (
|
||||
|
|
|
@ -194,6 +194,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
) }
|
||||
/>
|
||||
) }
|
||||
<SelectControlMenuSlot />
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, act, screen, waitFor } from '@testing-library/react';
|
||||
import { render, act, screen } from '@testing-library/react';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { ProductAttribute } from '@woocommerce/data';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -73,8 +72,8 @@ jest.mock( '@wordpress/data', () => ( {
|
|||
|
||||
jest.mock( '@woocommerce/components', () => ( {
|
||||
__esModule: true,
|
||||
__experimentalSelectControlMenuSlot: () => <div></div>,
|
||||
ListItem: ( { children }: { children: JSX.Element } ) => children,
|
||||
__experimentalSelectControlMenuSlot: () => null,
|
||||
Sortable: ( {
|
||||
onOrderChange,
|
||||
children,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Icon } from '@wordpress/components';
|
||||
import { plus } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
import { ProductCategory } from '@woocommerce/data';
|
||||
import { __experimentalSelectControlMenuItemProps as MenuItemProps } from '@woocommerce/components';
|
||||
|
||||
type CategoryFieldAddNewItemProps = {
|
||||
item: Pick< ProductCategory, 'id' | 'name' >;
|
||||
highlightedIndex: number;
|
||||
items: Pick< ProductCategory, 'id' | 'name' >[];
|
||||
} & Pick<
|
||||
MenuItemProps< Pick< ProductCategory, 'id' | 'name' > >,
|
||||
'getItemProps'
|
||||
>;
|
||||
|
||||
export const CategoryFieldAddNewItem: React.FC<
|
||||
CategoryFieldAddNewItemProps
|
||||
> = ( { item, highlightedIndex, getItemProps, items } ) => {
|
||||
const index = items.findIndex( ( i ) => i.id === item.id );
|
||||
return (
|
||||
<li
|
||||
{ ...getItemProps( {
|
||||
item,
|
||||
index,
|
||||
} ) }
|
||||
className={ classNames(
|
||||
'woocommerce-category-field-dropdown__item is-new',
|
||||
{
|
||||
item_highlighted: highlightedIndex === index,
|
||||
}
|
||||
) }
|
||||
>
|
||||
<div className="woocommerce-category-field-dropdown__item-content">
|
||||
<Icon
|
||||
className="category-field-dropdown__toggle"
|
||||
icon={ plus }
|
||||
size={ 20 }
|
||||
/>
|
||||
{ sprintf( __( 'Create "%s"', 'woocommerce' ), item.name ) }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -18,7 +18,6 @@ export type CategoryTreeItem = {
|
|||
type CategoryFieldItemProps = {
|
||||
item: CategoryTreeItem;
|
||||
selectedIds: number[];
|
||||
onSelect: ( item: ProductCategory ) => void;
|
||||
items: Pick< ProductCategory, 'id' | 'name' >[];
|
||||
highlightedIndex: number;
|
||||
openParent?: () => void;
|
||||
|
@ -30,7 +29,6 @@ type CategoryFieldItemProps = {
|
|||
export const CategoryFieldItem: React.FC< CategoryFieldItemProps > = ( {
|
||||
item,
|
||||
selectedIds = [],
|
||||
onSelect,
|
||||
items,
|
||||
highlightedIndex,
|
||||
openParent,
|
||||
|
@ -89,7 +87,7 @@ export const CategoryFieldItem: React.FC< CategoryFieldItemProps > = ( {
|
|||
<CheckboxControl
|
||||
label={ item.data.name }
|
||||
checked={ selectedIds.includes( item.data.id ) }
|
||||
onChange={ () => item.data /*&& onSelect( item.data )*/ }
|
||||
onChange={ () => item.data }
|
||||
/>
|
||||
</div>
|
||||
{ children.length > 0 ? (
|
||||
|
@ -107,7 +105,6 @@ export const CategoryFieldItem: React.FC< CategoryFieldItemProps > = ( {
|
|||
key={ child.data.id }
|
||||
item={ child }
|
||||
selectedIds={ selectedIds }
|
||||
onSelect={ onSelect }
|
||||
items={ items }
|
||||
highlightedIndex={ highlightedIndex }
|
||||
openParent={ () => ! isOpen && setIsOpen( true ) }
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
&.is-new {
|
||||
.category-field-dropdown__toggle {
|
||||
margin-right: $gap-smaller;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__item-content {
|
||||
height: 48px;
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
import { useMemo, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
selectControlStateChangeTypes,
|
||||
Spinner,
|
||||
__experimentalSelectControl as SelectControl,
|
||||
__experimentalSelectControlMenuSlot as MenuSlot,
|
||||
__experimentalSelectControlMenu as Menu,
|
||||
} from '@woocommerce/components';
|
||||
import { ProductCategory } from '@woocommerce/data';
|
||||
|
@ -17,6 +19,8 @@ import { debounce } from 'lodash';
|
|||
import './category-field.scss';
|
||||
import { CategoryFieldItem, CategoryTreeItem } from './category-field-item';
|
||||
import { useCategorySearch } from './use-category-search';
|
||||
import { CreateCategoryModal } from './create-category-modal';
|
||||
import { CategoryFieldAddNewItem } from './category-field-add-new-item';
|
||||
|
||||
type CategoryFieldProps = {
|
||||
label: string;
|
||||
|
@ -36,9 +40,10 @@ function getSelectedWithParents(
|
|||
): Pick< ProductCategory, 'id' | 'name' >[] {
|
||||
selected.push( { id: item.id, name: item.name } );
|
||||
|
||||
const parentId = item.parent
|
||||
? item.parent
|
||||
: treeKeyValues[ item.id ].parentID;
|
||||
const parentId =
|
||||
item.parent !== undefined
|
||||
? item.parent
|
||||
: treeKeyValues[ item.id ].parentID;
|
||||
if (
|
||||
parentId > 0 &&
|
||||
treeKeyValues[ parentId ] &&
|
||||
|
@ -69,8 +74,11 @@ export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
|||
searchCategories,
|
||||
getFilteredItems,
|
||||
} = useCategorySearch();
|
||||
const [ showCreateNewModal, setShowCreateNewModal ] = useState( false );
|
||||
const [ searchValue, setSearchValue ] = useState( '' );
|
||||
|
||||
const onInputChange = ( searchString?: string ) => {
|
||||
setSearchValue( searchString || '' );
|
||||
searchCategories( searchString || '' );
|
||||
};
|
||||
|
||||
|
@ -80,6 +88,10 @@ export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
|||
);
|
||||
|
||||
const onSelect = ( itemId: number, selected: boolean ) => {
|
||||
if ( itemId === -99 ) {
|
||||
setShowCreateNewModal( true );
|
||||
return;
|
||||
}
|
||||
if ( selected ) {
|
||||
const item = categoryTreeKeyValues[ itemId ].data;
|
||||
if ( item ) {
|
||||
|
@ -96,65 +108,97 @@ export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
|||
}
|
||||
};
|
||||
|
||||
const categoryFieldGetFilteredItems = (
|
||||
allItems: Pick< ProductCategory, 'id' | 'name' >[],
|
||||
inputValue: string,
|
||||
selectedItems: Pick< ProductCategory, 'id' | 'name' >[]
|
||||
) => {
|
||||
const filteredItems = getFilteredItems(
|
||||
allItems,
|
||||
inputValue,
|
||||
selectedItems
|
||||
);
|
||||
if (
|
||||
inputValue.length > 0 &&
|
||||
! isSearching &&
|
||||
! filteredItems.find(
|
||||
( item ) => item.name.toLowerCase() === inputValue.toLowerCase()
|
||||
)
|
||||
) {
|
||||
return [
|
||||
...filteredItems,
|
||||
{
|
||||
id: -99,
|
||||
name: inputValue,
|
||||
},
|
||||
];
|
||||
}
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
const selectedIds = value.map( ( item ) => item.id );
|
||||
const selectControlItems = categoriesSelectList;
|
||||
|
||||
return (
|
||||
<SelectControl< Pick< ProductCategory, 'id' | 'name' > >
|
||||
className="woocommerce-category-field-dropdown components-base-control"
|
||||
multiple
|
||||
items={ selectControlItems }
|
||||
label={ label }
|
||||
selected={ value }
|
||||
getItemLabel={ ( item ) => item?.name || '' }
|
||||
getItemValue={ ( item ) => item?.id || '' }
|
||||
onSelect={ ( item ) => {
|
||||
if ( item ) {
|
||||
onSelect( item.id, ! selectedIds.includes( item.id ) );
|
||||
}
|
||||
} }
|
||||
onRemove={ ( item ) => item && onSelect( item.id, false ) }
|
||||
onInputChange={ searchDelayed }
|
||||
getFilteredItems={ getFilteredItems }
|
||||
placeholder={ value.length === 0 ? placeholder : '' }
|
||||
stateReducer={ ( state, actionAndChanges ) => {
|
||||
const { changes, type } = actionAndChanges;
|
||||
switch ( type ) {
|
||||
case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem:
|
||||
return {
|
||||
...changes,
|
||||
inputValue: state.inputValue,
|
||||
};
|
||||
case selectControlStateChangeTypes.ItemClick:
|
||||
return {
|
||||
...changes,
|
||||
isOpen: true,
|
||||
inputValue: state.inputValue,
|
||||
highlightedIndex: state.highlightedIndex,
|
||||
};
|
||||
default:
|
||||
return changes;
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ ( {
|
||||
items,
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
selectItem,
|
||||
highlightedIndex,
|
||||
} ) => {
|
||||
const rootItems =
|
||||
items.length > 0
|
||||
? items.filter(
|
||||
( item ) =>
|
||||
categoryTreeKeyValues[ item.id ]
|
||||
?.parentID === 0
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<SelectControl< Pick< ProductCategory, 'id' | 'name' > >
|
||||
className="woocommerce-category-field-dropdown components-base-control"
|
||||
multiple
|
||||
items={ categoriesSelectList }
|
||||
label={ label }
|
||||
selected={ value }
|
||||
getItemLabel={ ( item ) => item?.name || '' }
|
||||
getItemValue={ ( item ) => item?.id || '' }
|
||||
onSelect={ ( item ) => {
|
||||
if ( item ) {
|
||||
onSelect( item.id, ! selectedIds.includes( item.id ) );
|
||||
}
|
||||
} }
|
||||
onRemove={ ( item ) => item && onSelect( item.id, false ) }
|
||||
onInputChange={ searchDelayed }
|
||||
getFilteredItems={ categoryFieldGetFilteredItems }
|
||||
placeholder={ value.length === 0 ? placeholder : '' }
|
||||
stateReducer={ ( state, actionAndChanges ) => {
|
||||
const { changes, type } = actionAndChanges;
|
||||
switch ( type ) {
|
||||
case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem:
|
||||
return {
|
||||
...changes,
|
||||
inputValue: state.inputValue,
|
||||
};
|
||||
case selectControlStateChangeTypes.ItemClick:
|
||||
if (
|
||||
changes.selectedItem &&
|
||||
changes.selectedItem.id === -99
|
||||
) {
|
||||
return changes;
|
||||
}
|
||||
return {
|
||||
...changes,
|
||||
isOpen: true,
|
||||
inputValue: state.inputValue,
|
||||
highlightedIndex: state.highlightedIndex,
|
||||
};
|
||||
default:
|
||||
return changes;
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ ( {
|
||||
items,
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
} ) => {
|
||||
const rootItems =
|
||||
items.length > 0
|
||||
? items.filter(
|
||||
( item ) =>
|
||||
categoryTreeKeyValues[ item.id ]
|
||||
?.parentID === 0 || item.id === -99
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<Menu
|
||||
isOpen={ isOpen }
|
||||
getMenuProps={ getMenuProps }
|
||||
|
@ -169,26 +213,57 @@ export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
|||
</li>
|
||||
) }
|
||||
{ isOpen &&
|
||||
rootItems.map( ( item ) => (
|
||||
<CategoryFieldItem
|
||||
key={ `${ item.id }` }
|
||||
item={
|
||||
categoryTreeKeyValues[ item.id ]
|
||||
}
|
||||
highlightedIndex={
|
||||
highlightedIndex
|
||||
}
|
||||
selectedIds={ selectedIds }
|
||||
onSelect={ selectItem }
|
||||
items={ items }
|
||||
getItemProps={ getItemProps }
|
||||
/>
|
||||
) ) }
|
||||
rootItems.map( ( item ) => {
|
||||
return item.id === -99 ? (
|
||||
<CategoryFieldAddNewItem
|
||||
key={ `${ item.id }` }
|
||||
item={ item }
|
||||
highlightedIndex={
|
||||
highlightedIndex
|
||||
}
|
||||
items={ items }
|
||||
getItemProps={ getItemProps }
|
||||
/>
|
||||
) : (
|
||||
<CategoryFieldItem
|
||||
key={ `${ item.id }` }
|
||||
item={
|
||||
categoryTreeKeyValues[
|
||||
item.id
|
||||
]
|
||||
}
|
||||
highlightedIndex={
|
||||
highlightedIndex
|
||||
}
|
||||
selectedIds={ selectedIds }
|
||||
items={ items }
|
||||
getItemProps={ getItemProps }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
</>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
} }
|
||||
</SelectControl>
|
||||
);
|
||||
} }
|
||||
</SelectControl>
|
||||
<MenuSlot />
|
||||
{ showCreateNewModal && (
|
||||
<CreateCategoryModal
|
||||
initialCategoryName={ searchValue }
|
||||
onCancel={ () => setShowCreateNewModal( false ) }
|
||||
onCreate={ ( newCategory ) => {
|
||||
onChange(
|
||||
getSelectedWithParents(
|
||||
[ ...value ],
|
||||
newCategory,
|
||||
categoryTreeKeyValues
|
||||
)
|
||||
);
|
||||
setShowCreateNewModal( false );
|
||||
onInputChange( '' );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
.woocommerce-create-new-category-modal {
|
||||
min-width: 650px;
|
||||
|
||||
&__buttons {
|
||||
margin-top: $gap-larger;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $gap-smaller;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.category-field-dropdown {
|
||||
&__menu {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-select-control__popover-menu {
|
||||
margin-top: -$gap-small;
|
||||
}
|
||||
.woocommerce-select-control__popover-menu-container {
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
padding: 0 $gap-smaller 0 $gap-small;
|
||||
|
||||
> .category-field-dropdown__item:not(:first-child) {
|
||||
.category-field-dropdown__item-content {
|
||||
border-top: 1px solid $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { Button, Modal, Spinner, TextControl } from '@wordpress/components';
|
||||
import { useDebounce } from '@wordpress/compose';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import {
|
||||
__experimentalSelectControl as SelectControl,
|
||||
__experimentalSelectControlMenu as Menu,
|
||||
} from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME,
|
||||
ProductCategory,
|
||||
} from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './create-category-modal.scss';
|
||||
import { useCategorySearch } from './use-category-search';
|
||||
import { CategoryFieldItem } from './category-field-item';
|
||||
|
||||
type CreateCategoryModalProps = {
|
||||
initialCategoryName?: string;
|
||||
onCancel: () => void;
|
||||
onCreate: ( newCategory: ProductCategory ) => void;
|
||||
};
|
||||
|
||||
export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
||||
initialCategoryName,
|
||||
onCancel,
|
||||
onCreate,
|
||||
} ) => {
|
||||
const {
|
||||
categoriesSelectList,
|
||||
isSearching,
|
||||
categoryTreeKeyValues,
|
||||
searchCategories,
|
||||
getFilteredItems,
|
||||
} = useCategorySearch();
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const [ isCreating, setIsCreating ] = useState( false );
|
||||
const { createProductCategory, invalidateResolutionForStoreSelector } =
|
||||
useDispatch( EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME );
|
||||
const [ categoryName, setCategoryName ] = useState(
|
||||
initialCategoryName || ''
|
||||
);
|
||||
const [ categoryParent, setCategoryParent ] = useState< Pick<
|
||||
ProductCategory,
|
||||
'id' | 'name'
|
||||
> | null >( null );
|
||||
|
||||
const onSave = async () => {
|
||||
recordEvent( 'product_category_add', {
|
||||
new_product_page: true,
|
||||
} );
|
||||
setIsCreating( true );
|
||||
try {
|
||||
const newCategory: ProductCategory = await createProductCategory( {
|
||||
name: categoryName,
|
||||
parent: categoryParent ? categoryParent.id : undefined,
|
||||
} );
|
||||
invalidateResolutionForStoreSelector( 'getProductCategories' );
|
||||
setIsCreating( false );
|
||||
onCreate( newCategory );
|
||||
} catch ( e ) {
|
||||
createNotice(
|
||||
'error',
|
||||
__( 'Failed to create category.', 'woocommerce' )
|
||||
);
|
||||
setIsCreating( false );
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSearch = useDebounce( searchCategories, 250 );
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Create category', 'woocommerce' ) }
|
||||
onRequestClose={ () => onCancel() }
|
||||
className="woocommerce-create-new-category-modal"
|
||||
>
|
||||
<div className="woocommerce-create-new-category-modal__wrapper">
|
||||
<TextControl
|
||||
label={ __( 'Name', 'woocommerce' ) }
|
||||
name="Tops"
|
||||
value={ categoryName }
|
||||
onChange={ setCategoryName }
|
||||
/>
|
||||
<SelectControl< Pick< ProductCategory, 'id' | 'name' > >
|
||||
items={ categoriesSelectList }
|
||||
label={ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Parent category {{optional/}}',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
optional: (
|
||||
<span className="woocommerce-product-form__optional-input">
|
||||
{ __( '(optional)', 'woocommerce' ) }
|
||||
</span>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
selected={ categoryParent }
|
||||
onSelect={ ( item ) => item && setCategoryParent( item ) }
|
||||
onRemove={ () => setCategoryParent( null ) }
|
||||
onInputChange={ debouncedSearch }
|
||||
getFilteredItems={ getFilteredItems }
|
||||
getItemLabel={ ( item ) => item?.name || '' }
|
||||
getItemValue={ ( item ) => item?.id || '' }
|
||||
>
|
||||
{ ( {
|
||||
items,
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
highlightedIndex,
|
||||
getItemProps,
|
||||
} ) => {
|
||||
return (
|
||||
<Menu
|
||||
isOpen={ isOpen }
|
||||
getMenuProps={ getMenuProps }
|
||||
className="woocommerce-category-field-dropdown__menu"
|
||||
>
|
||||
{ [
|
||||
isSearching ? (
|
||||
<div
|
||||
key="loading-spinner"
|
||||
className="woocommerce-category-field-dropdown__item"
|
||||
>
|
||||
<div className="woocommerce-category-field-dropdown__item-content">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
) : null,
|
||||
...items
|
||||
.filter(
|
||||
( item ) =>
|
||||
categoryTreeKeyValues[ item.id ]
|
||||
?.parentID === 0
|
||||
)
|
||||
.map( ( item ) => {
|
||||
return (
|
||||
<CategoryFieldItem
|
||||
key={ `${ item.id }` }
|
||||
item={
|
||||
categoryTreeKeyValues[
|
||||
item.id
|
||||
]
|
||||
}
|
||||
selectedIds={
|
||||
categoryParent
|
||||
? [
|
||||
categoryParent.id,
|
||||
]
|
||||
: []
|
||||
}
|
||||
items={ items }
|
||||
highlightedIndex={
|
||||
highlightedIndex
|
||||
}
|
||||
getItemProps={
|
||||
getItemProps
|
||||
}
|
||||
/>
|
||||
);
|
||||
} ),
|
||||
].filter(
|
||||
( item ): item is JSX.Element =>
|
||||
item !== null
|
||||
) }
|
||||
</Menu>
|
||||
);
|
||||
} }
|
||||
</SelectControl>
|
||||
<div className="woocommerce-create-new-category-modal__buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={ () => onCancel() }
|
||||
disabled={ isCreating }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
disabled={ categoryName.length === 0 || isCreating }
|
||||
isBusy={ isCreating }
|
||||
onClick={ () => {
|
||||
onSave();
|
||||
} }
|
||||
>
|
||||
{ __( 'Save', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -207,12 +207,12 @@ export const useCategorySearch = () => {
|
|||
}, [ initialCategories ] );
|
||||
|
||||
const searchCategories = useCallback(
|
||||
async ( search: string ): Promise< CategoryTreeItem[] > => {
|
||||
lastSearchValue.current = search;
|
||||
async ( search?: string ): Promise< CategoryTreeItem[] > => {
|
||||
lastSearchValue.current = search || '';
|
||||
if ( ! isAsync && initialCategories.length > 0 ) {
|
||||
return getCategoriesTreeWithMissingParents(
|
||||
[ ...initialCategories ],
|
||||
search
|
||||
search || ''
|
||||
).then( ( categoryData ) => {
|
||||
setCategoriesAndNewItem( categoryData );
|
||||
return categoryData[ 1 ];
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add add new option for the category dropdown within the product MVP
|
Loading…
Reference in New Issue