fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,242 @@
# QueryControls
## Development Guidelines
### Usage
```jsx
import { useState } from 'react';
import { QueryControls } from '@wordpress/components';
const QUERY_DEFAULTS = {
category: 1,
categories: [
{
id: 1,
name: 'Category 1',
parent: 0,
},
{
id: 2,
name: 'Category 1b',
parent: 1,
},
{
id: 3,
name: 'Category 2',
parent: 0,
},
],
maxItems: 20,
minItems: 1,
numberOfItems: 10,
order: 'asc',
orderBy: 'title',
};
const MyQueryControls = () => {
const [ query, setQuery ] = useState( QUERY_DEFAULTS );
const { category, categories, maxItems, minItems, numberOfItems, order, orderBy } = query;
const updateQuery = ( newQuery ) => {
setQuery( { ...query, ...newQuery } );
};
return (
<QueryControls
{ ...{ maxItems, minItems, numberOfItems, order, orderBy } }
onOrderByChange={ ( newOrderBy ) => updateQuery( { orderBy: newOrderBy } ) }
onOrderChange={ ( newOrder ) => updateQuery( { order: newOrder } ) }
categoriesList={ categories }
selectedCategoryId={ category }
onCategoryChange={ ( newCategory ) => updateQuery( { category: newCategory } ) }
onNumberOfItemsChange={ ( newNumberOfItems ) =>
updateQuery( { numberOfItems: newNumberOfItems } )
}
/>
);
};
```
### Multiple category selector
The `QueryControls` component now supports multiple category selection, to replace the single category selection available so far. To enable it use the component with the new props instead: `categorySuggestions` in place of `categoriesList` and the `selectedCategories` array instead of `selectedCategoryId` like so:
```jsx
const QUERY_DEFAULTS = {
orderBy: 'title',
order: 'asc',
selectedCategories: [
{
id: 1,
value: 'Category 1',
parent: 0,
},
{
id: 2,
value: 'Category 1b',
parent: 1,
},
],
categories: {
'Category 1': {
id: 1,
name: 'Category 1',
parent: 0,
},
'Category 1b': {
id: 2,
name: 'Category 1b',
parent: 1,
},
'Category 2': {
id: 3,
name: 'Category 2',
parent: 0,
},
},
numberOfItems: 10,
};
const MyQueryControls = () => {
const [ query, setQuery ] = useState( QUERY_DEFAULTS );
const { orderBy, order, selectedCategories, categories, numberOfItems } = query;
const updateQuery = ( newQuery ) => {
setQuery( { ...query, ...newQuery } );
};
return (
<QueryControls
{ ...{ orderBy, order, numberOfItems } }
onOrderByChange={ ( newOrderBy ) => updateQuery( { orderBy: newOrderBy } ) }
onOrderChange={ ( newOrder ) => updateQuery( { order: newOrder } ) }
categorySuggestions={ categories }
selectedCategories={ selectedCategories }
onCategoryChange={ ( category ) => updateQuery( { selectedCategories: category } ) }
onNumberOfItemsChange={ ( newNumberOfItems ) =>
updateQuery( { numberOfItems: newNumberOfItems } )
}
/>
);
};
```
The format of the categories list also needs to be updated to match the expected type for the category suggestions.
### Props
#### `authorList`: `Author[]`
An array of the authors to select from.
- Required: No
- Platform: Web
#### `categoriesList`: `Category[]`
An array of categories. When passed in conjunction with the `onCategoryChange` prop, it causes the component to render UI that allows selecting one category at a time.
- Required: No
- Platform: Web
#### `categorySuggestions`: `Record< Category[ 'name' ], Category >`
An object of categories with the category name as the key. When passed in conjunction with the `onCategoryChange` prop, it causes the component to render UI that enables multiple selection.
- Required: No
- Platform: Web
#### `maxItems`: `number`
The maximum number of items.
- Required: No
- Default: 100
- Platform: Web
#### `minItems`: `number`
The minimum number of items.
- Required: No
- Default: 1
- Platform: Web
#### `numberOfItems`: `number`
The selected number of items to retrieve via the query.
- Required: No
- Platform: Web
#### `onAuthorChange`: `( newAuthor: string ) => void`
A function that receives the new author value. If not specified, the author controls are not rendered.
- Required: No
- Platform: Web
#### `onCategoryChange`: `( newCategory: string ) => void | FormTokenFieldProps[ 'onChange' ]`
A function that receives the new category value. If not specified, the category controls are not rendered.
The function's signature changes depending on whether multiple category selection is enabled or not.
- Required: No
- Platform: Web
#### `onNumberOfItemsChange`: `( newNumber?: number ) => void`
A function that receives the new number of items. If not specified, then the number of items range control is not rendered.
- Required: No
- Platform: Web
#### `onOrderChange`: `( newOrder: 'asc' | 'desc' ) => void`
A function that receives the new order value. If this prop or the `onOrderByChange` prop are not specified, then the order controls are not rendered.
- Required: No
- Platform: Web
#### `onOrderByChange`: `( newOrderBy: 'date' | 'title' ) => void`
A function that receives the new orderby value. If this prop or the `onOrderChange` prop are not specified, then the order controls are not rendered.
- Required: No
- Platform: Web
#### `order`: `'asc' | 'desc'`
The order in which to retrieve posts.
- Required: No
- Platform: Web
#### `orderBy`: `'date' | 'title'`
The meta key by which to order posts.
- Required: No
- Platform: Web
#### `selectedAuthorId`: `number`
The selected author ID.
- Required: No
- Platform: Web
#### `selectedCategories`: `Category[]`
The selected categories for the `categorySuggestions` prop.
- Required: No
- Platform: Web
#### `selectedCategoryId`: `number`
The selected category for the `categoriesList` prop.
- Required: No
- Platform: Web

View File

@@ -0,0 +1,35 @@
/**
* Internal dependencies
*/
import { buildTermsTree } from './terms';
import TreeSelect from '../tree-select';
import type { AuthorSelectProps } from './types';
export default function AuthorSelect( {
__next40pxDefaultSize,
label,
noOptionLabel,
authorList,
selectedAuthorId,
onChange: onChangeProp,
}: AuthorSelectProps ) {
if ( ! authorList ) return null;
const termsTree = buildTermsTree( authorList );
return (
<TreeSelect
{ ...{
label,
noOptionLabel,
onChange: onChangeProp,
} }
tree={ termsTree }
selectedId={
selectedAuthorId !== undefined
? String( selectedAuthorId )
: undefined
}
__nextHasNoMarginBottom
__next40pxDefaultSize={ __next40pxDefaultSize }
/>
);
}

View File

@@ -0,0 +1,44 @@
/**
* Internal dependencies
*/
import { buildTermsTree } from './terms';
import TreeSelect from '../tree-select';
/**
* WordPress dependencies
*/
import { useMemo } from '@wordpress/element';
import type { CategorySelectProps } from './types';
export default function CategorySelect( {
__next40pxDefaultSize,
label,
noOptionLabel,
categoriesList,
selectedCategoryId,
onChange: onChangeProp,
...props
}: CategorySelectProps ) {
const termsTree = useMemo( () => {
return buildTermsTree( categoriesList );
}, [ categoriesList ] );
return (
<TreeSelect
{ ...{
label,
noOptionLabel,
onChange: onChangeProp,
} }
tree={ termsTree }
selectedId={
selectedCategoryId !== undefined
? String( selectedCategoryId )
: undefined
}
{ ...props }
__nextHasNoMarginBottom
__next40pxDefaultSize={ __next40pxDefaultSize }
/>
);
}

View File

@@ -0,0 +1,101 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useCallback, memo } from '@wordpress/element';
/**
* Internal dependencies
*/
import { RangeControl, SelectControl } from '../';
import CategorySelect from './category-select';
const DEFAULT_MIN_ITEMS = 1;
const DEFAULT_MAX_ITEMS = 100;
const options = [
{
label: __( 'Newest to oldest' ),
value: 'date/desc',
},
{
label: __( 'Oldest to newest' ),
value: 'date/asc',
},
{
/* translators: label for ordering posts by title in ascending order */
label: __( 'A → Z' ),
value: 'title/asc',
},
{
/* translators: label for ordering posts by title in descending order */
label: __( 'Z → A' ),
value: 'title/desc',
},
];
const QueryControls = memo(
( {
categoriesList,
selectedCategoryId,
numberOfItems,
order,
orderBy,
maxItems = DEFAULT_MAX_ITEMS,
minItems = DEFAULT_MIN_ITEMS,
onCategoryChange,
onNumberOfItemsChange,
onOrderChange,
onOrderByChange,
} ) => {
const onChange = useCallback(
( value ) => {
const [ newOrderBy, newOrder ] = value.split( '/' );
if ( newOrder !== order ) {
onOrderChange( newOrder );
}
if ( newOrderBy !== orderBy ) {
onOrderByChange( newOrderBy );
}
},
[ order, orderBy, onOrderByChange, onOrderChange ]
);
return (
<>
{ onOrderChange && onOrderByChange && (
<SelectControl
label={ __( 'Order by' ) }
value={ `${ orderBy }/${ order }` }
options={ options }
onChange={ onChange }
hideCancelButton={ true }
/>
) }
{ onCategoryChange && (
<CategorySelect
categoriesList={ categoriesList }
label={ __( 'Category' ) }
noOptionLabel={ __( 'All' ) }
selectedCategoryId={ selectedCategoryId }
onChange={ onCategoryChange }
hideCancelButton={ true }
/>
) }
{ onNumberOfItemsChange && (
<RangeControl
__next40pxDefaultSize
label={ __( 'Number of items' ) }
value={ numberOfItems }
onChange={ onNumberOfItemsChange }
min={ minItems }
max={ maxItems }
required
/>
) }
</>
);
}
);
export default QueryControls;

View File

@@ -0,0 +1,200 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import AuthorSelect from './author-select';
import CategorySelect from './category-select';
import FormTokenField from '../form-token-field';
import RangeControl from '../range-control';
import SelectControl from '../select-control';
import { VStack } from '../v-stack';
import type {
QueryControlsProps,
QueryControlsWithMultipleCategorySelectionProps,
QueryControlsWithSingleCategorySelectionProps,
} from './types';
const DEFAULT_MIN_ITEMS = 1;
const DEFAULT_MAX_ITEMS = 100;
const MAX_CATEGORIES_SUGGESTIONS = 20;
function isSingleCategorySelection(
props: QueryControlsProps
): props is QueryControlsWithSingleCategorySelectionProps {
return 'categoriesList' in props;
}
function isMultipleCategorySelection(
props: QueryControlsProps
): props is QueryControlsWithMultipleCategorySelectionProps {
return 'categorySuggestions' in props;
}
/**
* Controls to query for posts.
*
* ```jsx
* const MyQueryControls = () => (
* <QueryControls
* { ...{ maxItems, minItems, numberOfItems, order, orderBy } }
* onOrderByChange={ ( newOrderBy ) => {
* updateQuery( { orderBy: newOrderBy } )
* }
* onOrderChange={ ( newOrder ) => {
* updateQuery( { order: newOrder } )
* }
* categoriesList={ categories }
* selectedCategoryId={ category }
* onCategoryChange={ ( newCategory ) => {
* updateQuery( { category: newCategory } )
* }
* onNumberOfItemsChange={ ( newNumberOfItems ) => {
* updateQuery( { numberOfItems: newNumberOfItems } )
* } }
* />
* );
* ```
*/
export function QueryControls( {
__next40pxDefaultSize = false,
authorList,
selectedAuthorId,
numberOfItems,
order,
orderBy,
maxItems = DEFAULT_MAX_ITEMS,
minItems = DEFAULT_MIN_ITEMS,
onAuthorChange,
onNumberOfItemsChange,
onOrderChange,
onOrderByChange,
// Props for single OR multiple category selection are not destructured here,
// but instead are destructured inline where necessary.
...props
}: QueryControlsProps ) {
return (
<VStack spacing="4" className="components-query-controls">
{ [
onOrderChange && onOrderByChange && (
<SelectControl
__nextHasNoMarginBottom
__next40pxDefaultSize={ __next40pxDefaultSize }
key="query-controls-order-select"
label={ __( 'Order by' ) }
value={ `${ orderBy }/${ order }` }
options={ [
{
label: __( 'Newest to oldest' ),
value: 'date/desc',
},
{
label: __( 'Oldest to newest' ),
value: 'date/asc',
},
{
/* translators: label for ordering posts by title in ascending order */
label: __( 'A → Z' ),
value: 'title/asc',
},
{
/* translators: label for ordering posts by title in descending order */
label: __( 'Z → A' ),
value: 'title/desc',
},
] }
onChange={ ( value ) => {
if ( typeof value !== 'string' ) {
return;
}
const [ newOrderBy, newOrder ] = value.split( '/' );
if ( newOrder !== order ) {
onOrderChange(
newOrder as NonNullable<
QueryControlsProps[ 'order' ]
>
);
}
if ( newOrderBy !== orderBy ) {
onOrderByChange(
newOrderBy as NonNullable<
QueryControlsProps[ 'orderBy' ]
>
);
}
} }
/>
),
isSingleCategorySelection( props ) &&
props.categoriesList &&
props.onCategoryChange && (
<CategorySelect
__next40pxDefaultSize={ __next40pxDefaultSize }
key="query-controls-category-select"
categoriesList={ props.categoriesList }
label={ __( 'Category' ) }
noOptionLabel={ __( 'All' ) }
selectedCategoryId={ props.selectedCategoryId }
onChange={ props.onCategoryChange }
/>
),
isMultipleCategorySelection( props ) &&
props.categorySuggestions &&
props.onCategoryChange && (
<FormTokenField
__next40pxDefaultSize={ __next40pxDefaultSize }
__nextHasNoMarginBottom
key="query-controls-categories-select"
label={ __( 'Categories' ) }
value={
props.selectedCategories &&
props.selectedCategories.map( ( item ) => ( {
id: item.id,
// Keeping the fallback to `item.value` for legacy reasons,
// even if items of `selectedCategories` should not have a
// `value` property.
// @ts-expect-error
value: item.name || item.value,
} ) )
}
suggestions={ Object.keys(
props.categorySuggestions
) }
onChange={ props.onCategoryChange }
maxSuggestions={ MAX_CATEGORIES_SUGGESTIONS }
/>
),
onAuthorChange && (
<AuthorSelect
__next40pxDefaultSize={ __next40pxDefaultSize }
key="query-controls-author-select"
authorList={ authorList }
label={ __( 'Author' ) }
noOptionLabel={ __( 'All' ) }
selectedAuthorId={ selectedAuthorId }
onChange={ onAuthorChange }
/>
),
onNumberOfItemsChange && (
<RangeControl
__nextHasNoMarginBottom
__next40pxDefaultSize={ __next40pxDefaultSize }
key="query-controls-range-control"
label={ __( 'Number of items' ) }
value={ numberOfItems }
onChange={ onNumberOfItemsChange }
min={ minItems }
max={ maxItems }
required
/>
),
] }
</VStack>
);
}
export default QueryControls;

View File

@@ -0,0 +1,202 @@
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';
/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import QueryControls from '..';
import type {
Category,
QueryControlsWithSingleCategorySelectionProps,
QueryControlsWithMultipleCategorySelectionProps,
} from '../types';
const meta: Meta< typeof QueryControls > = {
title: 'Components/QueryControls',
component: QueryControls,
argTypes: {
numberOfItems: { control: { type: null } },
order: { control: { type: null } },
orderBy: { control: { type: null } },
selectedAuthorId: { control: { type: null } },
selectedCategories: { control: { type: null } },
selectedCategoryId: { control: { type: null } },
},
parameters: {
actions: { argTypesRegex: '^on.*' },
controls: { expanded: true },
docs: { canvas: { sourceState: 'shown' } },
},
};
export default meta;
export const Default: StoryFn< typeof QueryControls > = ( args ) => {
const {
onAuthorChange,
onCategoryChange,
onNumberOfItemsChange,
onOrderByChange,
onOrderChange,
...props
} = args as QueryControlsWithMultipleCategorySelectionProps;
const [ ownNumberOfItems, setOwnNumberOfItems ] = useState(
props.numberOfItems
);
const [ ownOrder, setOwnOrder ] = useState( props.order );
const [ ownOrderBy, setOwnOrderBy ] = useState( props.orderBy );
const [ ownSelectedAuthorId, setOwnSelectedAuthorId ] = useState(
props.selectedAuthorId
);
const [ ownSelectedCategories, setOwnSelectedCategories ] = useState(
props.selectedCategories
);
const handleCategoryChange: QueryControlsWithMultipleCategorySelectionProps[ 'onCategoryChange' ] =
( tokens ) => {
onCategoryChange?.( tokens );
const hasNoSuggestion = tokens.some(
( token ) =>
typeof token === 'string' &&
! props.categorySuggestions?.[ token ]
);
if ( hasNoSuggestion ) {
return;
}
const allCategories = tokens
.map( ( token ) => {
return typeof token === 'string'
? props.categorySuggestions?.[ token ]
: token;
} )
.filter( Boolean ) as Array< Required< Category > >;
setOwnSelectedCategories( allCategories );
};
return (
<QueryControls
{ ...props }
numberOfItems={ ownNumberOfItems }
onCategoryChange={ handleCategoryChange }
onOrderByChange={ ( newOrderBy ) => {
onOrderByChange?.( newOrderBy );
setOwnOrderBy( newOrderBy );
} }
onOrderChange={ ( newOrder ) => {
onOrderChange?.( newOrder );
setOwnOrder( newOrder );
} }
order={ ownOrder }
orderBy={ ownOrderBy }
onNumberOfItemsChange={ ( newNumber ) => {
onNumberOfItemsChange?.( newNumber );
setOwnNumberOfItems( newNumber );
} }
onAuthorChange={ ( newAuthor ) => {
onAuthorChange?.( newAuthor );
setOwnSelectedAuthorId( Number( newAuthor ) );
} }
selectedAuthorId={ ownSelectedAuthorId }
selectedCategories={ ownSelectedCategories }
/>
);
};
Default.args = {
authorList: [
{
id: 1,
name: 'admin',
},
{
id: 2,
name: 'editor',
},
],
categorySuggestions: {
TypeScript: {
id: 11,
name: 'TypeScript',
parent: 0,
},
JavaScript: {
id: 12,
name: 'JavaScript',
parent: 0,
},
},
selectedCategories: [
{
id: 11,
name: 'JavaScript',
parent: 0,
},
],
numberOfItems: 5,
order: 'desc',
orderBy: 'date',
selectedAuthorId: 1,
};
const SingleCategoryTemplate: StoryFn< typeof QueryControls > = ( args ) => {
const {
onAuthorChange,
onCategoryChange,
onNumberOfItemsChange,
onOrderByChange,
onOrderChange,
...props
} = args as QueryControlsWithSingleCategorySelectionProps;
const [ ownOrder, setOwnOrder ] = useState( props.order );
const [ ownOrderBy, setOwnOrderBy ] = useState( props.orderBy );
const [ ownSelectedCategoryId, setSelectedCategoryId ] = useState(
props.selectedCategoryId
);
const handleCategoryChange: QueryControlsWithSingleCategorySelectionProps[ 'onCategoryChange' ] =
( newCategory ) => {
onCategoryChange?.( newCategory );
setSelectedCategoryId( Number( newCategory ) );
};
return (
<QueryControls
{ ...props }
onCategoryChange={ handleCategoryChange }
onOrderByChange={ ( newOrderBy ) => {
setOwnOrderBy( newOrderBy );
} }
onOrderChange={ ( newOrder ) => {
onOrderChange?.( newOrder );
setOwnOrder( newOrder );
} }
order={ ownOrder }
orderBy={ ownOrderBy }
selectedCategoryId={ ownSelectedCategoryId }
/>
);
};
export const SelectSingleCategory = SingleCategoryTemplate.bind( {} );
SelectSingleCategory.args = {
categoriesList: [
{
id: 11,
name: 'TypeScript',
parent: 0,
},
{
id: 12,
name: 'JavaScript',
parent: 0,
},
],
selectedCategoryId: 11,
};

View File

@@ -0,0 +1,68 @@
/**
* Internal dependencies
*/
import type {
Author,
Category,
TermWithParentAndChildren,
TermsByParent,
} from './types';
const ensureParentsAreDefined = (
terms: TermWithParentAndChildren[]
): terms is ( TermWithParentAndChildren & { parent: number } )[] => {
return terms.every( ( term ) => term.parent !== null );
};
/**
* Returns terms in a tree form.
*
* @param flatTerms Array of terms in flat format.
*
* @return Terms in tree format.
*/
export function buildTermsTree( flatTerms: readonly ( Author | Category )[] ) {
const flatTermsWithParentAndChildren: TermWithParentAndChildren[] =
flatTerms.map( ( term ) => ( {
children: [],
parent: null,
...term,
id: String( term.id ),
} ) );
// We use a custom type guard here to ensure that the parent property is
// defined on all terms. The type of the `parent` property is `number | null`
// and we need to ensure that it is `number`. This is because we use the
// `parent` property as a key in the `termsByParent` object.
if ( ! ensureParentsAreDefined( flatTermsWithParentAndChildren ) ) {
return flatTermsWithParentAndChildren;
}
const termsByParent = flatTermsWithParentAndChildren.reduce(
( acc: TermsByParent, term ) => {
const { parent } = term;
if ( ! acc[ parent ] ) {
acc[ parent ] = [];
}
acc[ parent ].push( term );
return acc;
},
{}
);
const fillWithChildren = (
terms: TermWithParentAndChildren[]
): TermWithParentAndChildren[] => {
return terms.map( ( term ) => {
const children = termsByParent[ term.id ];
return {
...term,
children:
children && children.length
? fillWithChildren( children )
: [],
};
} );
};
return fillWithChildren( termsByParent[ '0' ] || [] );
}

View File

@@ -0,0 +1,83 @@
/**
* Internal dependencies
*/
import { buildTermsTree } from '../terms';
describe( 'buildTermsTree()', () => {
it( 'Should return same array as input with null parent and empty children added if parent is never specified.', () => {
const input = Object.freeze( [
{ id: 2232, name: 'foo', dummy: true },
{ id: 2245, name: 'baz', dummy: true },
] );
const output = Object.freeze( [
{
id: '2232',
name: 'foo',
parent: null,
children: [],
dummy: true,
},
{
id: '2245',
name: 'baz',
parent: null,
children: [],
dummy: true,
},
] );
const termsTreem = buildTermsTree( input );
expect( termsTreem ).toEqual( output );
} );
it( 'Should return same array as input with empty children added if all the elements are top level', () => {
const input = Object.freeze( [
{ id: 2232, name: 'foo', parent: 0, dummy: true },
{ id: 2245, name: 'baz', parent: 0, dummy: false },
] );
const output = [
{ id: '2232', name: 'foo', parent: 0, children: [], dummy: true },
{ id: '2245', name: 'baz', parent: 0, children: [], dummy: false },
];
const termsTreem = buildTermsTree( input );
expect( termsTreem ).toEqual( output );
} );
it( 'Should return element with its child if a child exists', () => {
const input = Object.freeze( [
{ id: 2232, name: 'foo', parent: 0 },
{ id: 2245, name: 'baz', parent: 2232 },
] );
const output = [
{
id: '2232',
name: 'foo',
parent: 0,
children: [
{ id: '2245', name: 'baz', parent: 2232, children: [] },
],
},
];
const termsTreem = buildTermsTree( input );
expect( termsTreem ).toEqual( output );
} );
it( 'Should return elements with multiple children and elements with no children', () => {
const input = Object.freeze( [
{ id: 2232, name: 'a', parent: 0 },
{ id: 2245, name: 'b', parent: 2232 },
{ id: 2249, name: 'c', parent: 0 },
{ id: 2246, name: 'd', parent: 2232 },
] );
const output = [
{
id: '2232',
name: 'a',
parent: 0,
children: [
{ id: '2245', name: 'b', parent: 2232, children: [] },
{ id: '2246', name: 'd', parent: 2232, children: [] },
],
},
{ id: '2249', name: 'c', parent: 0, children: [] },
];
const termsTreem = buildTermsTree( input );
expect( termsTreem ).toEqual( output );
} );
} );

View File

@@ -0,0 +1,159 @@
/**
* Internal dependencies
*/
import type { FormTokenFieldProps } from '../form-token-field/types';
import type { TreeSelectProps } from '../tree-select/types';
export type Author = {
id: number;
name: string;
};
export type Category = {
id: number;
name: string;
parent: number;
};
export type TermWithParentAndChildren = {
id: string;
name: string;
parent: number | null;
children: TermWithParentAndChildren[];
};
export type TermsByParent = Record< string, TermWithParentAndChildren[] >;
export type CategorySelectProps = Pick<
TreeSelectProps,
'label' | 'noOptionLabel'
> & {
categoriesList: Category[];
onChange: ( newCategory: string ) => void;
selectedCategoryId?: Category[ 'id' ];
__next40pxDefaultSize: boolean;
};
export type AuthorSelectProps = Pick<
TreeSelectProps,
'label' | 'noOptionLabel'
> & {
authorList?: Author[];
onChange: ( newAuthor: string ) => void;
selectedAuthorId?: Author[ 'id' ];
__next40pxDefaultSize: boolean;
};
type Order = 'asc' | 'desc';
type OrderBy = 'date' | 'title';
type BaseQueryControlsProps = {
/**
* An array of the authors to select from.
*/
authorList?: AuthorSelectProps[ 'authorList' ];
/**
* The maximum number of items.
*
* @default 100
*/
maxItems?: number;
/**
* The minimum number of items.
*
* @default 1
*/
minItems?: number;
/**
* The selected number of items to retrieve via the query.
*/
numberOfItems?: number;
/**
* A function that receives the new author value.
* If not specified, the author controls are not rendered.
*/
onAuthorChange?: AuthorSelectProps[ 'onChange' ];
/**
* A function that receives the new number of items.
* If not specified, then the number of items
* range control is not rendered.
*/
onNumberOfItemsChange?: ( newNumber?: number ) => void;
/**
* A function that receives the new order value.
* If this prop or the `onOrderByChange` prop are not specified,
* then the order controls are not rendered.
*/
onOrderChange?: ( newOrder: Order ) => void;
/**
* A function that receives the new orderby value.
* If this prop or the `onOrderChange` prop are not specified,
* then the order controls are not rendered.
*/
onOrderByChange?: ( newOrderBy: OrderBy ) => void;
/**
* The order in which to retrieve posts.
*/
order?: Order;
/**
* The meta key by which to order posts.
*/
orderBy?: OrderBy;
/**
* The selected author ID.
*/
selectedAuthorId?: AuthorSelectProps[ 'selectedAuthorId' ];
/**
* Start opting into the larger default height that will become the
* default size in a future version.
*
* @default false
*/
__next40pxDefaultSize?: boolean;
};
export type QueryControlsWithSingleCategorySelectionProps =
BaseQueryControlsProps & {
/**
* An array of categories. When passed in conjunction with the
* `onCategoryChange` prop, it causes the component to render UI that allows
* selecting one category at a time.
*/
categoriesList?: CategorySelectProps[ 'categoriesList' ];
/**
* The selected category for the `categoriesList` prop.
*/
selectedCategoryId?: CategorySelectProps[ 'selectedCategoryId' ];
/**
* A function that receives the new category value. If not specified, the
* category controls are not rendered.
* The function's signature changes depending on whether multiple category
* selection is enabled or not.
*/
onCategoryChange?: CategorySelectProps[ 'onChange' ];
};
export type QueryControlsWithMultipleCategorySelectionProps =
BaseQueryControlsProps & {
/**
* An object of categories with the category name as the key. When passed in
* conjunction with the `onCategoryChange` prop, it causes the component to
* render UI that enables multiple selection.
*/
categorySuggestions?: Record< Category[ 'name' ], Category >;
/**
* The selected categories for the `categorySuggestions` prop.
*/
selectedCategories?: Category[];
/**
* A function that receives the new category value. If not specified, the
* category controls are not rendered.
* The function's signature changes depending on whether multiple category
* selection is enabled or not.
*/
onCategoryChange?: FormTokenFieldProps[ 'onChange' ];
};
export type QueryControlsProps =
| QueryControlsWithSingleCategorySelectionProps
| QueryControlsWithMultipleCategorySelectionProps;