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

First step for Preset Post Types #48

Merged
merged 12 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions @types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ declare namespace EntitiesSearch {
type Options<V> = Set<ControlOption<V>>;
type Value = string | number;

interface QueryArguments<V>
// TODO Can we convert QueryArguments to an Immutable Map?
interface QueryArguments
extends Partial<
Readonly<{
exclude: Set<V>;
include: Set<V>;
exclude: Set<string | number>;
include: Set<string | number>;
fields: EntitiesSearch.SearchQueryFields;
[p: string]: unknown;
}>
Expand All @@ -34,6 +35,8 @@ declare namespace EntitiesSearch {
url: string;
type: string;
subtype: string;
post_content: string;
post_excerpt: string;
}> {}

type SearchEntitiesFunction<E, K> = (
Expand Down Expand Up @@ -99,6 +102,7 @@ declare namespace EntitiesSearch {
/*
* Api
*/
// TODO Better to convert the SearchQueryFields to a Set.
type SearchQueryFields = ReadonlyArray<
keyof EntitiesSearch.SearchEntityFields
>;
Expand Down
14 changes: 14 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,17 @@ NOTE: The function does not handle possible request exceptions.

The `searchEntities` will automatically abort the requests with the same parameters if a new request is made before the previous one is completed.



## `searchEntitiesOptions`

This function is a wrapper for the `searchEntites` which perform a small additional task you might want to do quite ofter after retrieving the entities that is
to convert the entities to `EntitiesSearch.ControlOptions`. The argument taken are the same of the `searchEntities`.

### `createSearchEntitiesOptions`

This is a factory function, it returns a `searchEntitiesOptions` function preconfigured to search for a specific *root* kind.

For instance, if we call this function with `post` like `createSearchEntitiesOptions('post')` the returned function will be of type `searchEntitiesOptions` where you don't always have to pass the *root* type.

Most probably this will be the function you'll deal with most as it make easy to build a function you can consume with other components.
49 changes: 49 additions & 0 deletions docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ data down to the _Base Components_.
- `CompositeEntitiesByKind` - A composite component that displays a list of entities by kind. In this context _kind_
is `post` or `term` and _entity_ is the entity type of the content e.g. a `page` or a `category` term.

## Preset Components

- `PresetEntitiesByKind` - A top level component simplifying the DX with a preconfigured path of how to manage the data based on the context given.

## Composite Entities by Kind

The `CompositeEntitiesByKind` is a _generic_ component that can be used to display a list of entities by kind. It acts
Expand Down Expand Up @@ -188,6 +192,51 @@ export function MyComponent(props) {
Obviously depending on what you want to achieve you can use different _Base Components_ or create new ones, as mentioned
above the package comes with a set of _Base Components_ that can be used out of the box.

## Preset Entities By Kind

This component can be used for different scenarios since it allows you to pass all the necessary information to render the Selectors.

It gets passed the `entitiesFinder` which is the function performing the request to retrieve the Entities, but also it gives you the freedom to decide which components to render as Controls UI.

Below you can see how easy is to create a new control set compared to use the low level api `CompositeEntitiesByKind`.

```typescript jsx
const entitiesFinder = createSearchEntitiesOptions( 'post' );
const postTypesEntities = convertEntitiesToControlOptions(useQueryViewablePostTypes().records(), 'name', 'slug');

const props = {
entitiesFinder,
entities: new Set( [ 1, 2, 3 ] ),
onChangeEntities: (entities) => {
// Do something with the new set of entities
},
entitiesComponent: ToggleControl,
kind: new Set(['post']),
kindOptions: stubControlOptionsSet(),
onChangeKind: (kinds) => {
// Do something with the new set of kinds
},
kindComponent: ToggleControl
};

return (<PresetEntitiesByKind { ...props } />);
```

Therefore, the preset simplify and take care of some parts that you would have to configure otherwise. In the chapter below you can read the available properties.

### Properties

- `entitiesFinder` - The function which perform the search of the contextual entities. You can use the `createSearchEntitiesOptions` function by passing the `root` value such as `term` or `post`.
- `className` - For better customization you can pass your own custom classes.
- `entities` - A `EntitiesSearch.Entities` set of selected entities. For when you want some entities already selected.
- `onChangeEntities` - A task to perform when the selection change due to a user interaction.
- `entitiesComponent` - The component to use to render the control ui for the entities.
- `kind` - The predefined set of kind (e.g. post-types or taxonomies) you want to have already selected.
- `kindOptions` - A collection of `EntitiesSearch.ControlOption` among which the user can choose to retrieve the entities from.
- `onChangeKind` - A task to perform when the selection change due to a user interaction.
- `kindComponent` - The component to use to render the control ui for the kinds.
- `entitiesFields` - Additional fields you want to retrieve and have available within your `entitiesComponent` and `kindComponent`. For more info read the [Control Option](./control-option.md) documentation.

## About Singular Base Components

The _Composite Component_ always give a collection of Entities and Kind even though you are consuming a Single* _Base
Expand Down
19 changes: 19 additions & 0 deletions sources/client/src/api/create-search-entities-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* External dependencies
*/
import EntitiesSearch from '@types';

/**
* Internal dependencies
*/
import { Set } from '../models/set';
import { searchEntitiesOptions } from './search-entities-options';

export function createSearchEntitiesOptions< E >( type: string ) {
return async (
phrase: string,
postTypes: EntitiesSearch.Kind< string >,
queryArguments?: EntitiesSearch.QueryArguments
): Promise< Set< EntitiesSearch.ControlOption< E > > > =>
searchEntitiesOptions( type, phrase, postTypes, queryArguments );
}
36 changes: 36 additions & 0 deletions sources/client/src/api/search-entities-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* External dependencies
*/
import EntitiesSearch from '@types';

/**
* Internal dependencies
*/
import { Set } from '../models/set';
import { searchEntities } from './search-entities';
import { convertEntitiesToControlOptions } from '../utils/convert-entities-to-control-options';

export async function searchEntitiesOptions< E >(
type: string,
phrase: string,
postTypes: EntitiesSearch.Kind< string >,
queryArguments?: EntitiesSearch.QueryArguments
): Promise< Set< EntitiesSearch.ControlOption< E > > > {
const postsEntities =
await searchEntities< EntitiesSearch.SearchEntityFields >(
type,
postTypes,
phrase,
queryArguments
);

const { fields = [] } = queryArguments ?? {};
const [ label = 'title', value = 'id', ...extraFields ] = fields;

return convertEntitiesToControlOptions(
postsEntities,
label,
value,
...extraFields
);
}
2 changes: 1 addition & 1 deletion sources/client/src/api/search-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function searchEntities< E >(
type: string,
subtype: Set< string >,
phrase: string,
queryArguments?: EntitiesSearch.QueryArguments< string >
queryArguments?: EntitiesSearch.QueryArguments
): Promise< Set< E > > {
const {
exclude,
Expand Down
158 changes: 158 additions & 0 deletions sources/client/src/components/preset-entities-by-kind.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* External dependencies
*/
import EntitiesSearch from '@types';
import React, { JSX } from 'react';
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { createHigherOrderComponent } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { CompositeEntitiesByKind } from './composite-entities-by-kind';
import { SearchControl } from './search-control';
import { Set } from '../models/set';

type EntitiesValue = EntitiesSearch.Value;
type Entities = EntitiesSearch.Entities< EntitiesValue >;
type KindValue = EntitiesSearch.Kind< string > | string;

type EntitiesFinder = (
phrase: string,
kind: EntitiesSearch.Kind< string >,
queryArguments?: EntitiesSearch.QueryArguments
) => Promise< Set< EntitiesSearch.ControlOption< EntitiesValue > > >;

type EntitiesComponent = React.ComponentType<
EntitiesSearch.BaseControl< EntitiesValue >
>;
type KindComponent = React.ComponentType<
EntitiesSearch.BaseControl< KindValue >
>;

type PublicComponentProps = {
entitiesFinder: EntitiesFinder;
className?: string;
entities: Entities;
onChangeEntities: ( values: Entities ) => void;
entitiesComponent: EntitiesComponent;
kind: KindValue;
kindOptions: EntitiesSearch.Options< string >;
onChangeKind: ( values: KindValue ) => void;
kindComponent: KindComponent;
entitiesFields?: EntitiesSearch.QueryArguments[ 'fields' ];
};

interface PrivateComponentProps
extends Pick<
EntitiesSearch.CompositeEntitiesKinds< EntitiesValue, string >,
'kind' | 'entities'
> {
className?: string;
entitiesFinder: EntitiesFinder;
kindComponent: KindComponent;
entitiesComponent: EntitiesComponent;
}

function PrivateComponent( props: PrivateComponentProps ): JSX.Element {
const className = classnames( 'wes-preset-entities-by-kind', {
// @ts-ignore
[ props.className ]: !! props.className,
} );

return (
<div className={ className }>
<CompositeEntitiesByKind
entities={ props.entities }
kind={ props.kind }
searchEntities={ props.entitiesFinder }
>
{ ( _entities, _kind, search ) => (
<>
<props.kindComponent { ..._kind } />
<SearchControl onChange={ search } />
<props.entitiesComponent { ..._entities } />
</>
) }
</CompositeEntitiesByKind>
</div>
);
}

const withDataBound = createHigherOrderComponent<
React.ComponentType< PrivateComponentProps >,
React.ComponentType< PublicComponentProps >
>(
( Component ) => ( props ) => {
const {
kind,
entities,
onChangeEntities,
kindOptions,
onChangeKind,
entitiesFields,
kindComponent,
entitiesComponent,
entitiesFinder,
..._props
} = props;

const kindValue = narrowKindValue( props.kind );

const _entities = {
value: entities,
onChange: onChangeEntities,
};

const _kind = {
value: kindValue,
options: kindOptions,
onChange: onChangeKind,
};

const _entitiesFinder = entitiesFinderWithExtraFields(
entitiesFinder,
entitiesFields
);

return (
<Component
entities={ _entities }
kind={ _kind }
entitiesFinder={ _entitiesFinder }
kindComponent={ kindComponent }
entitiesComponent={ entitiesComponent }
{ ..._props }
/>
);
},
'withDataBound'
);

function entitiesFinderWithExtraFields(
entitiesFinder: EntitiesFinder,
entitiesFields?: EntitiesSearch.QueryArguments[ 'fields' ]
): EntitiesFinder {
return (
phrase: string,
kind: EntitiesSearch.Kind< string >,
queryArguments?: EntitiesSearch.QueryArguments
) =>
entitiesFinder( phrase, kind, {
...queryArguments,
fields: [
...( queryArguments?.fields ?? [ 'title', 'id' ] ),
...( entitiesFields ?? [] ),
],
} );
}

function narrowKindValue( value: KindValue ): EntitiesSearch.Kind< string > {
return typeof value === 'string' ? new Set( [ value ] ) : value;
}

export const PresetEntitiesByKind = withDataBound( PrivateComponent );
3 changes: 3 additions & 0 deletions sources/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export * from './api/search-entities';
export * from './api/search-entities-options';
export * from './api/create-search-entities-options';

export * from './components/composite-entities-by-kind';
export * from './components/plural-select-control';
export * from './components/preset-entities-by-kind';
export * from './components/radio-control';
export * from './components/search-control';
export * from './components/singular-select-control';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function convertEntitiesToControlOptions<
entities: Set< EntitiesFields >,
labelKey: string,
valueKey: string,
...extraKeys: Array< string >
...extraKeys: ReadonlyArray< string >
): Set< EntitiesSearch.ControlOption< V > > {
return entities.map( ( entity ) => {
const label = entity[ labelKey ];
Expand Down
Loading