Update/34885 category field in product editor (#36869)
* Add initial custom meta box for product categories * Make use of TreeSelectControl * Update classnames * Display selected items and sync with most used tab * Always show placeholder and remove checklist container * Reactify category metabox tabs * Add create new category logic * Remove unused markup * Fix saving of empty category list * Add callback when input is cleared as well * Some small cleanup and refactoring. * Add changelog * Fix tree creation and style enqueue * Auto fix lint errors * Fix linting errors * Fix css lint errors * Add 100 limit, and address some PR feedback * Fix some styling and warnings * Remove unused code * Address PR feedback * Fix lint error * Fix lint errors * Address PR feedback * Fix lint error * Minor fixes and add tracking * Add debounce * Fix lint error * Allow custom min filter amount and fix menu not showing after escaping input * Allow single item to be cleared out of select control * Fix bug where typed values did not show up * Fix some styling issues * Allow parents to be individually selected * Address PR feedback and add error message * Add changelogs * Fix saving issue * Add client side sorting and stop clearing field upon selection * Update changelog * Create feature flag for async product categories dropdown * Fix lint errors * Fix linting
This commit is contained in:
parent
caf20d7989
commit
b42da82e50
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix issue where single item can not be cleared and text can not be selected upon click.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add minFilterQueryLength, individuallySelectParent, and clearOnSelect props.
|
|
@ -45,9 +45,8 @@ export const ComboBox = ( {
|
|||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if ( document.activeElement !== inputRef.current ) {
|
||||
event.preventDefault();
|
||||
inputRef.current.focus();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
|
|
@ -177,8 +177,13 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
items: filteredItems,
|
||||
selectedItem: multiple ? null : singleSelectedItem,
|
||||
itemToString: getItemLabel,
|
||||
onSelectedItemChange: ( { selectedItem } ) =>
|
||||
selectedItem && onSelect( selectedItem ),
|
||||
onSelectedItemChange: ( { selectedItem } ) => {
|
||||
if ( selectedItem ) {
|
||||
onSelect( selectedItem );
|
||||
} else if ( singleSelectedItem ) {
|
||||
onRemove( singleSelectedItem );
|
||||
}
|
||||
},
|
||||
onInputValueChange: ( { inputValue: value, ...changes } ) => {
|
||||
if ( value !== undefined ) {
|
||||
setInputValue( value );
|
||||
|
@ -193,8 +198,13 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
// Set input back to selected item if there is a selected item, blank otherwise.
|
||||
newChanges = {
|
||||
...changes,
|
||||
selectedItem:
|
||||
! changes.inputValue?.length && ! multiple
|
||||
? null
|
||||
: changes.selectedItem,
|
||||
inputValue:
|
||||
changes.selectedItem === state.selectedItem &&
|
||||
changes.inputValue?.length &&
|
||||
! multiple
|
||||
? getItemLabel( comboboxSingleSelectedItem )
|
||||
: '',
|
||||
|
|
|
@ -64,6 +64,7 @@ import { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, ROOT_VALUE } from './constants';
|
|||
* @param {string} [props.className] The class name for this component
|
||||
* @param {boolean} [props.disabled] Disables the component
|
||||
* @param {boolean} [props.includeParent] Includes parent with selection.
|
||||
* @param {boolean} [props.individuallySelectParent] Considers parent as a single item (default: false).
|
||||
* @param {boolean} [props.alwaysShowPlaceholder] Will always show placeholder (default: false)
|
||||
* @param {Option[]} [props.options] Options to show in the component
|
||||
* @param {string[]} [props.value] Selected values
|
||||
|
@ -71,6 +72,8 @@ import { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, ROOT_VALUE } from './constants';
|
|||
* @param {Function} [props.onChange] Callback when the selector changes
|
||||
* @param {(visible: boolean) => void} [props.onDropdownVisibilityChange] Callback when the visibility of the dropdown options is changed.
|
||||
* @param {Function} [props.onInputChange] Callback when the selector changes
|
||||
* @param {number} [props.minFilterQueryLength] Minimum input length to filter results by.
|
||||
* @param {boolean} [props.clearOnSelect] Clear input on select (default: true).
|
||||
* @return {JSX.Element} The component
|
||||
*/
|
||||
const TreeSelectControl = ( {
|
||||
|
@ -88,7 +91,10 @@ const TreeSelectControl = ( {
|
|||
onDropdownVisibilityChange = noop,
|
||||
onInputChange = noop,
|
||||
includeParent = false,
|
||||
individuallySelectParent = false,
|
||||
alwaysShowPlaceholder = false,
|
||||
minFilterQueryLength = 3,
|
||||
clearOnSelect = true,
|
||||
} ) => {
|
||||
let instanceId = useInstanceId( TreeSelectControl );
|
||||
instanceId = id ?? instanceId;
|
||||
|
@ -126,7 +132,8 @@ const TreeSelectControl = ( {
|
|||
|
||||
const filterQuery = inputControlValue.trim().toLowerCase();
|
||||
// we only trigger the filter when there are more than 3 characters in the input.
|
||||
const filter = filterQuery.length >= 3 ? filterQuery : '';
|
||||
const filter =
|
||||
filterQuery.length >= minFilterQueryLength ? filterQuery : '';
|
||||
|
||||
/**
|
||||
* Optimizes the performance for getting the tags info
|
||||
|
@ -419,9 +426,11 @@ const TreeSelectControl = ( {
|
|||
*/
|
||||
const handleParentChange = ( checked, option ) => {
|
||||
let newValue;
|
||||
const changedValues = option.leaves
|
||||
.filter( ( opt ) => opt.checked !== checked )
|
||||
.map( ( opt ) => opt.value );
|
||||
const changedValues = individuallySelectParent
|
||||
? []
|
||||
: option.leaves
|
||||
.filter( ( opt ) => opt.checked !== checked )
|
||||
.map( ( opt ) => opt.value );
|
||||
if ( includeParent && option.value !== ROOT_VALUE ) {
|
||||
changedValues.push( option.value );
|
||||
}
|
||||
|
@ -452,10 +461,12 @@ const TreeSelectControl = ( {
|
|||
handleSingleChange( checked, option, parent );
|
||||
}
|
||||
|
||||
onInputChange( '' );
|
||||
setInputControlValue( '' );
|
||||
if ( ! nodesExpanded.includes( option.parent ) ) {
|
||||
controlRef.current.focus();
|
||||
if ( clearOnSelect ) {
|
||||
onInputChange( '' );
|
||||
setInputControlValue( '' );
|
||||
if ( ! nodesExpanded.includes( option.parent ) ) {
|
||||
controlRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -475,6 +486,7 @@ const TreeSelectControl = ( {
|
|||
* @param {Event} e Event returned by the On Change function in the Input control
|
||||
*/
|
||||
const handleOnInputChange = ( e ) => {
|
||||
setTreeVisible( true );
|
||||
onInputChange( e.target.value );
|
||||
setInputControlValue( e.target.value );
|
||||
};
|
||||
|
|
|
@ -48,6 +48,9 @@ declare global {
|
|||
isDirty: () => boolean;
|
||||
};
|
||||
};
|
||||
getUserSetting?: ( name: string ) => string | undefined;
|
||||
setUserSetting?: ( name: string, value: string ) => void;
|
||||
deleteUserSetting?: ( name: string ) => void;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import { useDebounce } from '@wordpress/compose';
|
||||
import { TreeSelectControl } from '@woocommerce/components';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CATEGORY_TERM_NAME } from './category-handlers';
|
||||
import { CategoryTerm } from './popular-category-list';
|
||||
|
||||
declare const wc_product_category_metabox_params: {
|
||||
search_categories_nonce: string;
|
||||
};
|
||||
|
||||
type CategoryTreeItem = CategoryTerm & {
|
||||
children?: CategoryTreeItem[];
|
||||
};
|
||||
|
||||
type CategoryTreeItemLabelValue = {
|
||||
children: CategoryTreeItemLabelValue[];
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_DEBOUNCE_TIME = 250;
|
||||
|
||||
const categoryLibrary: Record< number, CategoryTreeItem > = {};
|
||||
function convertTreeToLabelValue(
|
||||
tree: CategoryTreeItem[],
|
||||
newTree: CategoryTreeItemLabelValue[] = []
|
||||
) {
|
||||
for ( const child of tree ) {
|
||||
const newItem = {
|
||||
label: child.name,
|
||||
value: child.term_id.toString(),
|
||||
children: [],
|
||||
};
|
||||
categoryLibrary[ child.term_id ] = child;
|
||||
newTree.push( newItem );
|
||||
if ( child.children?.length ) {
|
||||
convertTreeToLabelValue( child.children, newItem.children );
|
||||
}
|
||||
}
|
||||
newTree.sort(
|
||||
( a: CategoryTreeItemLabelValue, b: CategoryTreeItemLabelValue ) => {
|
||||
const nameA = a.label.toUpperCase();
|
||||
const nameB = b.label.toUpperCase();
|
||||
if ( nameA < nameB ) {
|
||||
return -1;
|
||||
}
|
||||
if ( nameA > nameB ) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
return newTree;
|
||||
}
|
||||
|
||||
async function getTreeItems( filter: string ) {
|
||||
const resp = await apiFetch< CategoryTreeItem[] >( {
|
||||
url: addQueryArgs(
|
||||
new URL( 'admin-ajax.php', getSetting( 'adminUrl' ) ).toString(),
|
||||
{
|
||||
term: filter,
|
||||
action: 'woocommerce_json_search_categories_tree',
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
security:
|
||||
wc_product_category_metabox_params.search_categories_nonce,
|
||||
}
|
||||
),
|
||||
method: 'GET',
|
||||
} );
|
||||
if ( resp ) {
|
||||
return convertTreeToLabelValue( Object.values( resp ) );
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export const AllCategoryList = forwardRef<
|
||||
{ resetInitialValues: () => void },
|
||||
{
|
||||
selectedCategoryTerms: CategoryTerm[];
|
||||
onChange: ( selected: CategoryTerm[] ) => void;
|
||||
}
|
||||
>( ( { selectedCategoryTerms, onChange }, ref ) => {
|
||||
const [ filter, setFilter ] = useState( '' );
|
||||
const [ treeItems, setTreeItems ] = useState<
|
||||
CategoryTreeItemLabelValue[]
|
||||
>( [] );
|
||||
|
||||
const searchCategories = useCallback(
|
||||
( value: string ) => {
|
||||
if ( value && value.length > 0 ) {
|
||||
recordEvent( 'product_category_search', {
|
||||
page: 'product',
|
||||
async: true,
|
||||
search_string_length: value.length,
|
||||
} );
|
||||
}
|
||||
getTreeItems( value ).then( ( res ) => {
|
||||
setTreeItems( Object.values( res ) );
|
||||
} );
|
||||
},
|
||||
[ setTreeItems ]
|
||||
);
|
||||
const searchCategoriesDebounced = useDebounce(
|
||||
searchCategories,
|
||||
DEFAULT_DEBOUNCE_TIME
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
searchCategoriesDebounced( filter );
|
||||
}, [ filter ] );
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
resetInitialValues() {
|
||||
getTreeItems( '' ).then( ( res ) => {
|
||||
setTreeItems( Object.values( res ) );
|
||||
} );
|
||||
},
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="product-add-category__tree-control">
|
||||
<TreeSelectControl
|
||||
alwaysShowPlaceholder={ true }
|
||||
options={ treeItems }
|
||||
value={ selectedCategoryTerms.map( ( category ) =>
|
||||
category.term_id.toString()
|
||||
) }
|
||||
onChange={ ( selectedCategoryIds: number[] ) => {
|
||||
onChange(
|
||||
selectedCategoryIds.map(
|
||||
( id ) => categoryLibrary[ id ]
|
||||
)
|
||||
);
|
||||
recordEvent( 'product_category_update', {
|
||||
page: 'product',
|
||||
async: true,
|
||||
selected: selectedCategoryIds.length,
|
||||
} );
|
||||
} }
|
||||
selectAllLabel={ false }
|
||||
onInputChange={ setFilter }
|
||||
placeholder={ __( 'Add category', 'woocommerce' ) }
|
||||
includeParent={ true }
|
||||
minFilterQueryLength={ 2 }
|
||||
clearOnSelect={ false }
|
||||
individuallySelectParent={ true }
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
// Adding tagchecklist class to make use of already existing styling for the selected categories.
|
||||
className="categorychecklist form-no-clear tagchecklist"
|
||||
id={ CATEGORY_TERM_NAME + 'checklist' }
|
||||
>
|
||||
{ selectedCategoryTerms.map( ( selectedCategory ) => (
|
||||
<li key={ selectedCategory.term_id }>
|
||||
<button
|
||||
type="button"
|
||||
className="ntdelbutton"
|
||||
onClick={ () => {
|
||||
const newSelectedItems =
|
||||
selectedCategoryTerms.filter(
|
||||
( category ) =>
|
||||
category.term_id !==
|
||||
selectedCategory.term_id
|
||||
);
|
||||
onChange( newSelectedItems );
|
||||
} }
|
||||
>
|
||||
<span
|
||||
className="remove-tag-icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="screen-reader-text">
|
||||
{ sprintf(
|
||||
__( 'Remove term: %s', 'woocommerce' ),
|
||||
selectedCategory.name
|
||||
) }
|
||||
</span>
|
||||
</button>
|
||||
{ selectedCategory.name }
|
||||
</li>
|
||||
) ) }
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
} );
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useCallback, useState } from '@wordpress/element';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import {
|
||||
useAsyncFilter,
|
||||
__experimentalSelectControl as SelectControl,
|
||||
} from '@woocommerce/components';
|
||||
import { useUser } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CATEGORY_TERM_NAME } from './category-handlers';
|
||||
import { CategoryTerm } from './popular-category-list';
|
||||
|
||||
declare const wc_product_category_metabox_params: {
|
||||
search_categories_nonce: string;
|
||||
};
|
||||
|
||||
function getCategoryTermLabel( item: CategoryTerm | null ): string {
|
||||
return item?.name || '';
|
||||
}
|
||||
|
||||
function getCategoryTermKey( item: CategoryTerm | null ): string {
|
||||
return String( item?.term_id );
|
||||
}
|
||||
|
||||
export const CategoryAddNew: React.FC< {
|
||||
selectedCategoryTerms: CategoryTerm[];
|
||||
onChange: ( selected: CategoryTerm[] ) => void;
|
||||
} > = ( { selectedCategoryTerms, onChange } ) => {
|
||||
const [ showAddNew, setShowAddNew ] = useState( false );
|
||||
const [ newCategoryName, setNewCategoryName ] = useState( '' );
|
||||
const [ categoryCreateError, setCategoryCreateError ] = useState( '' );
|
||||
const [ categoryParent, setCategoryParent ] = useState< CategoryTerm >();
|
||||
const [ fetchedItems, setFetchedItems ] = useState< CategoryTerm[] >( [] );
|
||||
const { currentUserCan } = useUser();
|
||||
|
||||
const canEditTerms = currentUserCan( 'edit_product_terms' );
|
||||
|
||||
const onCreate = ( event: React.MouseEvent< HTMLInputElement > ) => {
|
||||
event.preventDefault();
|
||||
if ( ! newCategoryName ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: newCategoryName,
|
||||
parent: categoryParent?.term_id ?? -1,
|
||||
};
|
||||
setCategoryCreateError( '' );
|
||||
apiFetch< {
|
||||
id: number;
|
||||
name: string;
|
||||
count: number;
|
||||
parent: number;
|
||||
} >( {
|
||||
path: '/wc/v3/products/categories',
|
||||
data,
|
||||
method: 'POST',
|
||||
} )
|
||||
.then( ( res ) => {
|
||||
if ( res ) {
|
||||
recordEvent( 'product_category_add', {
|
||||
category_id: res.id,
|
||||
parent_id: res.parent,
|
||||
parent_category: res.parent > 0 ? 'Other' : 'None',
|
||||
page: 'product',
|
||||
async: true,
|
||||
} );
|
||||
onChange( [
|
||||
...selectedCategoryTerms,
|
||||
{ term_id: res.id, name: res.name, count: res.count },
|
||||
] );
|
||||
setNewCategoryName( '' );
|
||||
setCategoryParent( undefined );
|
||||
setShowAddNew( false );
|
||||
}
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
if ( error && error.message ) {
|
||||
setCategoryCreateError( error.message );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
const filter: ( value: string ) => Promise< CategoryTerm[] > = useCallback(
|
||||
async ( value = '' ) => {
|
||||
setFetchedItems( [] );
|
||||
return apiFetch< CategoryTerm[] >( {
|
||||
url: addQueryArgs(
|
||||
new URL(
|
||||
'admin-ajax.php',
|
||||
getSetting( 'adminUrl' )
|
||||
).toString(),
|
||||
{
|
||||
term: value,
|
||||
action: 'woocommerce_json_search_categories',
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
security:
|
||||
wc_product_category_metabox_params.search_categories_nonce,
|
||||
}
|
||||
),
|
||||
method: 'GET',
|
||||
} ).then( ( response ) => {
|
||||
if ( response ) {
|
||||
setFetchedItems( Object.values( response ) );
|
||||
}
|
||||
return [];
|
||||
} );
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const { isFetching, ...selectProps } = useAsyncFilter< CategoryTerm >( {
|
||||
filter,
|
||||
} );
|
||||
|
||||
if ( ! canEditTerms ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={ CATEGORY_TERM_NAME + '-adder' }>
|
||||
<a
|
||||
id="product_cat-add-toggle"
|
||||
href={ '#taxonomy-' + CATEGORY_TERM_NAME }
|
||||
className="taxonomy-add-new"
|
||||
onClick={ () => setShowAddNew( ! showAddNew ) }
|
||||
aria-label={ __( 'Add new category', 'woocommerce' ) }
|
||||
>
|
||||
{ __( '+ Add new category', 'woocommerce' ) }
|
||||
</a>
|
||||
{ showAddNew && (
|
||||
<div id="product_cat-add" className="category-add">
|
||||
<label
|
||||
className="screen-reader-text"
|
||||
htmlFor="newproduct_cat"
|
||||
>
|
||||
{ __( 'Add new category', 'woocommerce' ) }
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="newproduct_cat"
|
||||
id="newproduct_cat"
|
||||
className="form-required"
|
||||
placeholder={ __( 'New category name', 'woocommerce' ) }
|
||||
value={ newCategoryName }
|
||||
onChange={ ( event ) =>
|
||||
setNewCategoryName( event.target.value )
|
||||
}
|
||||
aria-required="true"
|
||||
/>
|
||||
<label
|
||||
className="screen-reader-text"
|
||||
htmlFor="newproduct_cat_parent"
|
||||
>
|
||||
{ __( 'Parent category:', 'woocommerce' ) }
|
||||
</label>
|
||||
<SelectControl< CategoryTerm >
|
||||
{ ...selectProps }
|
||||
label={ __( 'Parent category:', 'woocommerce' ) }
|
||||
items={ fetchedItems }
|
||||
selected={ categoryParent || null }
|
||||
placeholder={ __( 'Find category', 'woocommerce' ) }
|
||||
onSelect={ setCategoryParent }
|
||||
getItemLabel={ getCategoryTermLabel }
|
||||
getItemValue={ getCategoryTermKey }
|
||||
onRemove={ () => setCategoryParent( undefined ) }
|
||||
/>
|
||||
{ categoryCreateError && (
|
||||
<p className="category-add__error">
|
||||
{ categoryCreateError }
|
||||
</p>
|
||||
) }
|
||||
<input
|
||||
type="button"
|
||||
id="product_cat-add-submit"
|
||||
className="button category-add-submit"
|
||||
value={ __( 'Add new category', 'woocommerce' ) }
|
||||
disabled={ ! newCategoryName.length }
|
||||
onClick={ onCreate }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
export const CATEGORY_TERM_NAME = 'product_cat';
|
||||
|
||||
export function getCategoryDataFromElement( element ) {
|
||||
if ( element && element.dataset && element.dataset.name ) {
|
||||
return {
|
||||
term_id: parseInt( element.value, 10 ),
|
||||
name: element.dataset.name,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSelectedCategoryData( container ) {
|
||||
if ( container ) {
|
||||
const selectedCategories = Array.from(
|
||||
container.querySelectorAll( ':scope > input[type=hidden]' )
|
||||
).map( ( categoryElement ) => {
|
||||
const id = getCategoryDataFromElement( categoryElement );
|
||||
categoryElement.remove();
|
||||
return id;
|
||||
} );
|
||||
return selectedCategories;
|
||||
}
|
||||
return [];
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useRef, useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CATEGORY_TERM_NAME } from './category-handlers';
|
||||
import { AllCategoryList } from './all-category-list';
|
||||
import { CategoryTerm, PopularCategoryList } from './popular-category-list';
|
||||
import { CategoryAddNew } from './category-add-new';
|
||||
|
||||
let initialTab = '';
|
||||
if ( window.getUserSetting ) {
|
||||
initialTab = window.getUserSetting( CATEGORY_TERM_NAME + '_tab' ) || '';
|
||||
}
|
||||
|
||||
const CATEGORY_POPULAR_TAB_ID = 'pop';
|
||||
const CATEGORY_ALL_TAB_ID = 'all';
|
||||
|
||||
const CategoryMetabox: React.FC< {
|
||||
initialSelected: CategoryTerm[];
|
||||
} > = ( { initialSelected } ) => {
|
||||
const [ selected, setSelected ] = useState( initialSelected );
|
||||
const allCategoryListRef = useRef< { resetInitialValues: () => void } >(
|
||||
null
|
||||
);
|
||||
const [ activeTab, setActiveTab ] = useState(
|
||||
initialTab === CATEGORY_POPULAR_TAB_ID
|
||||
? initialTab
|
||||
: CATEGORY_ALL_TAB_ID
|
||||
);
|
||||
return (
|
||||
<div
|
||||
id={ 'taxonomy-' + CATEGORY_TERM_NAME }
|
||||
className="categorydiv category-async-metabox"
|
||||
>
|
||||
<ul className="category-tabs">
|
||||
<li
|
||||
className={
|
||||
activeTab === CATEGORY_ALL_TAB_ID ? 'tabs' : ''
|
||||
}
|
||||
>
|
||||
<a
|
||||
href={
|
||||
'#' + CATEGORY_TERM_NAME + '-' + CATEGORY_ALL_TAB_ID
|
||||
}
|
||||
onClick={ ( event ) => {
|
||||
event.preventDefault();
|
||||
setActiveTab( CATEGORY_ALL_TAB_ID );
|
||||
if ( window.deleteUserSetting ) {
|
||||
window.deleteUserSetting(
|
||||
CATEGORY_TERM_NAME + '_tab'
|
||||
);
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ __( 'All items', 'woocommerce' ) }
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className={
|
||||
activeTab === CATEGORY_POPULAR_TAB_ID ? 'tabs' : ''
|
||||
}
|
||||
>
|
||||
<a
|
||||
href={
|
||||
'#' +
|
||||
CATEGORY_TERM_NAME +
|
||||
'-' +
|
||||
CATEGORY_POPULAR_TAB_ID
|
||||
}
|
||||
onClick={ ( event ) => {
|
||||
event.preventDefault();
|
||||
setActiveTab( CATEGORY_POPULAR_TAB_ID );
|
||||
if ( window.setUserSetting ) {
|
||||
window.setUserSetting(
|
||||
CATEGORY_TERM_NAME + '_tab',
|
||||
CATEGORY_POPULAR_TAB_ID
|
||||
);
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ __( 'Most used', 'woocommerce' ) }
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
className="tabs-panel"
|
||||
id={ CATEGORY_TERM_NAME + '-' + CATEGORY_POPULAR_TAB_ID }
|
||||
style={
|
||||
activeTab !== CATEGORY_POPULAR_TAB_ID
|
||||
? { display: 'none' }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<ul
|
||||
id={
|
||||
CATEGORY_TERM_NAME +
|
||||
'checklist-' +
|
||||
CATEGORY_POPULAR_TAB_ID
|
||||
}
|
||||
className="categorychecklist form-no-clear"
|
||||
>
|
||||
<PopularCategoryList
|
||||
selected={ selected }
|
||||
onChange={ setSelected }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
className="tabs-panel"
|
||||
id={ CATEGORY_TERM_NAME + '-' + CATEGORY_ALL_TAB_ID }
|
||||
style={
|
||||
activeTab !== CATEGORY_ALL_TAB_ID ? { display: 'none' } : {}
|
||||
}
|
||||
>
|
||||
<AllCategoryList
|
||||
selectedCategoryTerms={ selected }
|
||||
onChange={ setSelected }
|
||||
ref={ allCategoryListRef }
|
||||
/>
|
||||
</div>
|
||||
{ ( selected || [] ).map( ( sel ) => (
|
||||
<input
|
||||
key={ sel.term_id }
|
||||
type="hidden"
|
||||
value={ sel.term_id }
|
||||
name={ 'tax_input[' + CATEGORY_TERM_NAME + '][]' }
|
||||
/>
|
||||
) ) }
|
||||
{ selected.length === 0 && (
|
||||
<input
|
||||
type="hidden"
|
||||
value=""
|
||||
name={ 'tax_input[' + CATEGORY_TERM_NAME + '][]' }
|
||||
/>
|
||||
) }
|
||||
<CategoryAddNew
|
||||
selectedCategoryTerms={ selected }
|
||||
onChange={ ( sel ) => {
|
||||
setSelected( sel );
|
||||
if ( allCategoryListRef.current ) {
|
||||
allCategoryListRef.current.resetInitialValues();
|
||||
}
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryMetabox;
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, Suspense, lazy } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getSelectedCategoryData } from './category-handlers';
|
||||
import './style.scss';
|
||||
|
||||
const CategoryMetabox = lazy( () =>
|
||||
import( /* webpackChunkName: "category-metabox" */ './category-metabox' )
|
||||
);
|
||||
|
||||
const metaboxContainer = document.querySelector(
|
||||
'#taxonomy-product_cat-metabox'
|
||||
);
|
||||
if ( metaboxContainer ) {
|
||||
const initialSelected = getSelectedCategoryData(
|
||||
metaboxContainer.parentElement
|
||||
);
|
||||
render(
|
||||
<Suspense fallback={ null }>
|
||||
<CategoryMetabox initialSelected={ initialSelected } />
|
||||
</Suspense>,
|
||||
metaboxContainer
|
||||
);
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CATEGORY_TERM_NAME } from './category-handlers';
|
||||
|
||||
declare const wc_product_category_metabox_params: {
|
||||
search_taxonomy_terms_nonce: string;
|
||||
};
|
||||
|
||||
export type CategoryTerm = {
|
||||
name: string;
|
||||
term_id: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export const PopularCategoryList: React.FC< {
|
||||
selected: CategoryTerm[];
|
||||
onChange: ( selected: CategoryTerm[] ) => void;
|
||||
} > = ( { selected, onChange } ) => {
|
||||
const [ popularCategories, setPopularCategories ] = useState<
|
||||
CategoryTerm[]
|
||||
>( [] );
|
||||
|
||||
useEffect( () => {
|
||||
apiFetch< CategoryTerm[] >( {
|
||||
url: addQueryArgs(
|
||||
new URL(
|
||||
'admin-ajax.php',
|
||||
getSetting( 'adminUrl' )
|
||||
).toString(),
|
||||
{
|
||||
action: 'woocommerce_json_search_taxonomy_terms',
|
||||
taxonomy: CATEGORY_TERM_NAME,
|
||||
limit: 10,
|
||||
orderby: 'count',
|
||||
order: 'DESC',
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
security:
|
||||
wc_product_category_metabox_params.search_taxonomy_terms_nonce,
|
||||
}
|
||||
),
|
||||
method: 'GET',
|
||||
} ).then( ( res ) => {
|
||||
if ( res ) {
|
||||
setPopularCategories( res.filter( ( cat ) => cat.count > 0 ) );
|
||||
}
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
const selectedIds = selected.map( ( sel ) => sel.term_id );
|
||||
|
||||
return (
|
||||
<ul
|
||||
className="categorychecklist form-no-clear"
|
||||
id={ CATEGORY_TERM_NAME + 'checklist-pop' }
|
||||
>
|
||||
{ popularCategories.map( ( cat ) => {
|
||||
const categoryCheckboxId = `in-popular-${ CATEGORY_TERM_NAME }-${ cat.term_id }`;
|
||||
return (
|
||||
<li key={ cat.term_id } className="popular-category">
|
||||
<label
|
||||
className="selectit"
|
||||
htmlFor={ categoryCheckboxId }
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={ categoryCheckboxId }
|
||||
checked={ selectedIds.includes( cat.term_id ) }
|
||||
onChange={ () => {
|
||||
if ( selectedIds.includes( cat.term_id ) ) {
|
||||
onChange(
|
||||
selected.filter(
|
||||
( sel ) =>
|
||||
sel.term_id !== cat.term_id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
onChange( [ ...selected, cat ] );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
{ cat.name }
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
.product-add-category {
|
||||
&__tree-control {
|
||||
margin-top: $gap-smaller;
|
||||
|
||||
.woocommerce-tree-select-control {
|
||||
.components-base-control,
|
||||
.woocommerce-tree-select-control__tree {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.components-checkbox-control__label {
|
||||
min-height: $gap-larger;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: none;
|
||||
}
|
||||
.components-base-control .woocommerce-tree-select-control__control-input,
|
||||
.woocommerce-tree-select-control__option {
|
||||
font-size: 12px;
|
||||
}
|
||||
.components-checkbox-control__input {
|
||||
height: $gap;
|
||||
width: $gap;
|
||||
}
|
||||
.components-checkbox-control__checked {
|
||||
height: $gap + $gap-smallest;
|
||||
width: $gap + $gap-smallest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.categorydiv.category-async-metabox {
|
||||
#product_cat-all {
|
||||
overflow: visible;
|
||||
|
||||
.categorychecklist {
|
||||
max-height: 140px;
|
||||
margin-left: 0;
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: $gap-small;
|
||||
}
|
||||
.ntdelbutton {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.woocommerce-experimental-select-control__combo-box-wrapper {
|
||||
min-height: 30px;
|
||||
border-radius: $gap-smallest;
|
||||
}
|
||||
.woocommerce-experimental-select-control__menu-item {
|
||||
padding: 5px $gap-small;
|
||||
}
|
||||
.category-add {
|
||||
&__error {
|
||||
color: $error-red;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -67,6 +67,7 @@ const wpAdminScripts = [
|
|||
'order-tracking',
|
||||
'product-import-tracking',
|
||||
'variable-product-tour',
|
||||
'product-category-metabox',
|
||||
];
|
||||
const getEntryPoints = () => {
|
||||
const entryPoints = {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: major
|
||||
Type: update
|
||||
|
||||
Update Category product metabox with an async dropdown search control rendered with React.
|
|
@ -28,6 +28,7 @@
|
|||
"transient-notices": true,
|
||||
"woo-mobile-welcome": true,
|
||||
"wc-pay-promotion": true,
|
||||
"wc-pay-welcome-page": true
|
||||
"wc-pay-welcome-page": true,
|
||||
"async-product-editor-category-field": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"transient-notices": true,
|
||||
"woo-mobile-welcome": true,
|
||||
"wc-pay-promotion": true,
|
||||
"wc-pay-welcome-page": true
|
||||
"wc-pay-welcome-page": true,
|
||||
"async-product-editor-category-field": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
|
@ -197,6 +198,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
|
|||
wp_enqueue_script( 'iris' );
|
||||
wp_enqueue_script( 'woocommerce_admin' );
|
||||
wp_enqueue_script( 'wc-enhanced-select' );
|
||||
|
||||
wp_enqueue_script( 'jquery-ui-sortable' );
|
||||
wp_enqueue_script( 'jquery-ui-autocomplete' );
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/**
|
||||
* Product Categories meta box
|
||||
*
|
||||
* Display the product categories meta box.
|
||||
*
|
||||
* @package WooCommerce\Admin\Meta Boxes
|
||||
* @version 7.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WC_Meta_Box_Product_Categories Class.
|
||||
*/
|
||||
class WC_Meta_Box_Product_Categories {
|
||||
|
||||
/**
|
||||
* Output the metabox.
|
||||
*
|
||||
* @param WP_Post $post Current post object.
|
||||
* @param array $box {
|
||||
* Categories meta box arguments.
|
||||
*
|
||||
* @type string $id Meta box 'id' attribute.
|
||||
* @type string $title Meta box title.
|
||||
* @type callable $callback Meta box display callback.
|
||||
* @type array $args {
|
||||
* Extra meta box arguments.
|
||||
*
|
||||
* @type string $taxonomy Taxonomy. Default 'category'.
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public static function output( $post, $box ) {
|
||||
$categories_count = (int) wp_count_terms( 'product_cat' );
|
||||
|
||||
/**
|
||||
* Filters the category metabox search threshold, for when to render the typeahead field.
|
||||
*
|
||||
* @since 7.6.0
|
||||
*
|
||||
* @param number $threshold The default threshold.
|
||||
* @returns number The threshold that will be used.
|
||||
*/
|
||||
if ( $categories_count <= apply_filters( 'woocommerce_product_category_metabox_search_threshold', 100 ) && function_exists( 'post_categories_meta_box' ) ) {
|
||||
return post_categories_meta_box( $post, $box );
|
||||
}
|
||||
|
||||
$defaults = array( 'taxonomy' => 'category' );
|
||||
if ( ! isset( $box['args'] ) || ! is_array( $box['args'] ) ) {
|
||||
$args = array();
|
||||
} else {
|
||||
$args = $box['args'];
|
||||
}
|
||||
$parsed_args = wp_parse_args( $args, $defaults );
|
||||
$tax_name = $parsed_args['taxonomy'];
|
||||
$selected_categories = wp_get_object_terms( $post->ID, 'product_cat' );
|
||||
?>
|
||||
<div id="taxonomy-<?php echo esc_attr( $tax_name ); ?>-metabox"></div>
|
||||
<?php foreach ( (array) $selected_categories as $term ) { ?>
|
||||
<input
|
||||
type="hidden"
|
||||
value="<?php echo esc_attr( $term->term_id ); ?>"
|
||||
name="tax_input[<?php esc_attr( $tax_name ); ?>][]"
|
||||
data-name="<?php echo esc_attr( $term->name ); ?>"
|
||||
/>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
|
@ -157,6 +157,7 @@ class WC_AJAX {
|
|||
'json_search_downloadable_products_and_variations',
|
||||
'json_search_customers',
|
||||
'json_search_categories',
|
||||
'json_search_categories_tree',
|
||||
'json_search_taxonomy_terms',
|
||||
'json_search_product_attributes',
|
||||
'json_search_pages',
|
||||
|
@ -1769,12 +1770,13 @@ class WC_AJAX {
|
|||
wp_die();
|
||||
}
|
||||
|
||||
$show_empty = isset( $_GET['show_empty'] ) ? wp_validate_boolean( wc_clean( wp_unslash( $_GET['show_empty'] ) ) ) : false;
|
||||
$found_categories = array();
|
||||
$args = array(
|
||||
'taxonomy' => array( 'product_cat' ),
|
||||
'orderby' => 'id',
|
||||
'order' => 'ASC',
|
||||
'hide_empty' => true,
|
||||
'hide_empty' => ! $show_empty,
|
||||
'fields' => 'all',
|
||||
'name__like' => $search_text,
|
||||
);
|
||||
|
@ -1785,6 +1787,7 @@ class WC_AJAX {
|
|||
foreach ( $terms as $term ) {
|
||||
$term->formatted_name = '';
|
||||
|
||||
$ancestors = array();
|
||||
if ( $term->parent ) {
|
||||
$ancestors = array_reverse( get_ancestors( $term->term_id, 'product_cat' ) );
|
||||
foreach ( $ancestors as $ancestor ) {
|
||||
|
@ -1795,6 +1798,7 @@ class WC_AJAX {
|
|||
}
|
||||
}
|
||||
|
||||
$term->parents = $ancestors;
|
||||
$term->formatted_name .= $term->name . ' (' . $term->count . ')';
|
||||
$found_categories[ $term->term_id ] = $term;
|
||||
}
|
||||
|
@ -1803,6 +1807,75 @@ class WC_AJAX {
|
|||
wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $found_categories ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for categories and return json.
|
||||
*/
|
||||
public static function json_search_categories_tree() {
|
||||
ob_start();
|
||||
|
||||
check_ajax_referer( 'search-categories', 'security' );
|
||||
|
||||
if ( ! current_user_can( 'edit_products' ) ) {
|
||||
wp_die( -1 );
|
||||
}
|
||||
|
||||
$search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
|
||||
$number = isset( $_GET['number'] ) ? absint( $_GET['number'] ) : 50;
|
||||
|
||||
$args = array(
|
||||
'taxonomy' => array( 'product_cat' ),
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
'hide_empty' => false,
|
||||
'fields' => 'all',
|
||||
'number' => $number,
|
||||
'name__like' => $search_text,
|
||||
);
|
||||
|
||||
$terms = get_terms( $args );
|
||||
|
||||
$terms_map = array();
|
||||
|
||||
if ( $terms ) {
|
||||
foreach ( $terms as $term ) {
|
||||
$terms_map[ $term->term_id ] = $term;
|
||||
|
||||
if ( $term->parent ) {
|
||||
$ancestors = get_ancestors( $term->term_id, 'product_cat' );
|
||||
$current_child = $term;
|
||||
foreach ( $ancestors as $ancestor ) {
|
||||
if ( ! isset( $terms_map[ $ancestor ] ) ) {
|
||||
$ancestor_term = get_term( $ancestor, 'product_cat' );
|
||||
$terms_map[ $ancestor ] = $ancestor_term;
|
||||
}
|
||||
if ( ! $terms_map[ $ancestor ]->children ) {
|
||||
$terms_map[ $ancestor ]->children = array();
|
||||
}
|
||||
$item_exists = count(
|
||||
array_filter(
|
||||
$terms_map[ $ancestor ]->children,
|
||||
function( $term ) use ( $current_child ) {
|
||||
return $term->term_id === $current_child->term_id;
|
||||
}
|
||||
)
|
||||
) === 1;
|
||||
if ( ! $item_exists ) {
|
||||
$terms_map[ $ancestor ]->children[] = $current_child;
|
||||
}
|
||||
$current_child = $terms_map[ $ancestor ];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$parent_terms = array_filter(
|
||||
array_values( $terms_map ),
|
||||
function( $term ) {
|
||||
return 0 === $term->parent;
|
||||
}
|
||||
);
|
||||
wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $parent_terms ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for taxonomy terms and return json.
|
||||
*/
|
||||
|
@ -1819,11 +1892,12 @@ class WC_AJAX {
|
|||
$limit = isset( $_GET['limit'] ) ? absint( wp_unslash( $_GET['limit'] ) ) : null;
|
||||
$taxonomy = isset( $_GET['taxonomy'] ) ? wc_clean( wp_unslash( $_GET['taxonomy'] ) ) : '';
|
||||
$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : 'name';
|
||||
$order = isset( $_GET['order'] ) ? wc_clean( wp_unslash( $_GET['order'] ) ) : 'ASC';
|
||||
|
||||
$args = array(
|
||||
'taxonomy' => $taxonomy,
|
||||
'orderby' => $orderby,
|
||||
'order' => 'ASC',
|
||||
'order' => $order,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'all',
|
||||
'number' => $limit,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce Async Product Editor Category Field.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\AsyncProductEditorCategoryField;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Loads assets related to the async category field for the product editor.
|
||||
*/
|
||||
class Init {
|
||||
|
||||
const FEATURE_ID = 'async-product-editor-category-field';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( Features::is_enabled( self::FEATURE_ID ) ) {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
add_filter( 'woocommerce_taxonomy_args_product_cat', array( $this, 'add_metabox_args' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds meta_box_cb callback arguments for custom metabox.
|
||||
*
|
||||
* @param array $args Category taxonomy args.
|
||||
* @return array $args category taxonomy args.
|
||||
*/
|
||||
public function add_metabox_args( $args ) {
|
||||
if ( ! isset( $args['meta_box_cb'] ) ) {
|
||||
$args['meta_box_cb'] = 'WC_Meta_Box_Product_Categories::output';
|
||||
$args['meta_box_sanitize_cb'] = 'taxonomy_meta_box_sanitize_cb_checkboxes';
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts needed for the product form block editor.
|
||||
*/
|
||||
public function enqueue_scripts() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
WCAdminAssets::register_script( 'wp-admin-scripts', 'product-category-metabox', true );
|
||||
wp_localize_script(
|
||||
'wc-admin-product-category-metabox',
|
||||
'wc_product_category_metabox_params',
|
||||
array(
|
||||
'search_categories_nonce' => wp_create_nonce( 'search-categories' ),
|
||||
'search_taxonomy_terms_nonce' => wp_create_nonce( 'search-taxonomy-terms' ),
|
||||
)
|
||||
);
|
||||
wp_enqueue_script( 'product-category-metabox' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles needed for the rich text editor.
|
||||
*/
|
||||
public function enqueue_styles() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
$version = Constants::get_constant( 'WC_VERSION' );
|
||||
|
||||
wp_register_style(
|
||||
'woocommerce_admin_product_category_metabox_styles',
|
||||
WCAdminAssets::get_url( 'product-category-metabox/style', 'css' ),
|
||||
array(),
|
||||
$version
|
||||
);
|
||||
wp_style_add_data( 'woocommerce_admin_product_category_metabox_styles', 'rtl', 'replace' );
|
||||
|
||||
wp_enqueue_style( 'woocommerce_admin_product_category_metabox_styles' );
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue