Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update/34885 category field in product editor #36869

Merged
merged 40 commits into from Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d92a6d2
Add initial custom meta box for product categories
louwie17 Feb 3, 2023
db9379c
Make use of TreeSelectControl
louwie17 Feb 16, 2023
f43d195
Update classnames
louwie17 Feb 17, 2023
c0a1ab4
Display selected items and sync with most used tab
louwie17 Feb 21, 2023
e25f242
Always show placeholder and remove checklist container
louwie17 Feb 21, 2023
b457849
Reactify category metabox tabs
louwie17 Feb 21, 2023
66c288d
Add create new category logic
louwie17 Feb 21, 2023
640b133
Remove unused markup
louwie17 Feb 21, 2023
e51be55
Fix saving of empty category list
louwie17 Feb 21, 2023
1443008
Add callback when input is cleared as well
louwie17 Feb 21, 2023
d825a0f
Some small cleanup and refactoring.
louwie17 Feb 22, 2023
88e643c
Add changelog
louwie17 Feb 22, 2023
dc86d19
Fix tree creation and style enqueue
louwie17 Feb 22, 2023
80d4ee8
Auto fix lint errors
louwie17 Feb 22, 2023
1479c91
Fix linting errors
louwie17 Feb 23, 2023
ff4cad9
Fix css lint errors
louwie17 Feb 23, 2023
e1b2b5b
Add 100 limit, and address some PR feedback
louwie17 Mar 24, 2023
d6971de
Fix some styling and warnings
louwie17 Mar 24, 2023
67218d9
Remove unused code
louwie17 Mar 24, 2023
628b205
Address PR feedback
louwie17 Mar 24, 2023
53d9ceb
Fix lint error
louwie17 Mar 24, 2023
f9bab53
Fix lint errors
louwie17 Mar 24, 2023
03d2d48
Address PR feedback
louwie17 Apr 3, 2023
04a7f8e
Fix lint error
louwie17 Apr 4, 2023
832107e
Minor fixes and add tracking
louwie17 Apr 4, 2023
ebb0b74
Add debounce
louwie17 Apr 4, 2023
6952860
Fix lint error
louwie17 Apr 4, 2023
1ccbebe
Allow custom min filter amount and fix menu not showing after escapin…
louwie17 Apr 13, 2023
12b453c
Allow single item to be cleared out of select control
louwie17 Apr 13, 2023
65bf633
Fix bug where typed values did not show up
louwie17 Apr 13, 2023
2f9eb3a
Fix some styling issues
louwie17 Apr 13, 2023
5b6cc7b
Allow parents to be individually selected
louwie17 Apr 13, 2023
3fbea59
Address PR feedback and add error message
louwie17 Apr 13, 2023
2eb476d
Add changelogs
louwie17 Apr 13, 2023
8d1db92
Fix saving issue
louwie17 Apr 14, 2023
e8b6850
Add client side sorting and stop clearing field upon selection
louwie17 Apr 14, 2023
0032b93
Update changelog
louwie17 Apr 14, 2023
1a4cefd
Create feature flag for async product categories dropdown
louwie17 Apr 17, 2023
3b5b7f7
Fix lint errors
louwie17 Apr 17, 2023
28c8964
Fix linting
louwie17 Apr 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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.
Expand Up @@ -45,9 +45,8 @@ export const ComboBox = ( {
return;
}

event.preventDefault();

if ( document.activeElement !== inputRef.current ) {
event.preventDefault();
inputRef.current.focus();
event.stopPropagation();
}
Expand Down
Expand Up @@ -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 );
Expand All @@ -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 )
: '',
Expand Down
28 changes: 20 additions & 8 deletions packages/js/components/src/tree-select-control/index.js
Expand Up @@ -64,13 +64,16 @@ 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
* @param {number} [props.maxVisibleTags] The maximum number of tags to show. Undefined, 0 or less than 0 evaluates to "Show All".
* @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 = ( {
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 );
}
Expand Down Expand Up @@ -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();
}
}
};

Expand All @@ -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 );
};
Expand Down
3 changes: 3 additions & 0 deletions plugins/woocommerce-admin/client/typings/global.d.ts
Expand Up @@ -48,6 +48,9 @@ declare global {
isDirty: () => boolean;
};
};
getUserSetting?: ( name: string ) => string | undefined;
setUserSetting?: ( name: string, value: string ) => void;
deleteUserSetting?: ( name: string ) => void;
}
}

Expand Down
@@ -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"
mattsherman marked this conversation as resolved.
Show resolved Hide resolved
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>
</>
);
} );