Skip to content

Commit

Permalink
Introduce Preset Post Types
Browse files Browse the repository at this point in the history
  • Loading branch information
widoz committed Apr 11, 2024
1 parent 1a0e2c8 commit c979b20
Show file tree
Hide file tree
Showing 16 changed files with 793 additions and 119 deletions.
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

0 comments on commit c979b20

Please sign in to comment.