Skip to content

Commit

Permalink
[core-data] Document and add types for dynamic actions and selectors. (
Browse files Browse the repository at this point in the history
…WordPress#67668)

* [core-data] Document and add types for dynamic actions and selectors.

* Now that things are typed, we don't expect an error

* Put definitions first to allow it be overridden

* Use namespaces to avoid direct imports

* Remove unnecessary `ts-expect-error`

* Add notice for new entities

* Use existing Type instead of new PostType

* Dynamically create entity selectors and actions

* Remove unnecessary comment

* Export base type as UnstableBase

* Add template related types to base

* Get rid of one more @ts-expect-error

* Fix Site, Status and Revision types

* Add GlobalStyles

* Disable plural for global styles

* Add a note about "GlobalStyles"

* Fix type for gmt_offset

* Export and use TemplatePartArea
  • Loading branch information
manzoorwanijk authored Jan 6, 2025
1 parent 4b39807 commit 72a9996
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 15 deletions.
111 changes: 111 additions & 0 deletions packages/core-data/src/dynamic-entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Internal dependencies
*/
import type { GetRecordsHttpQuery, State } from './selectors';
import type * as ET from './entity-types';

export type WPEntityTypes< C extends ET.Context = 'edit' > = {
Comment: ET.Comment< C >;
GlobalStyles: ET.GlobalStylesRevision< C >;
Media: ET.Attachment< C >;
Menu: ET.NavMenu< C >;
MenuItem: ET.NavMenuItem< C >;
MenuLocation: ET.MenuLocation< C >;
Plugin: ET.Plugin< C >;
PostType: ET.Type< C >;
Revision: ET.PostRevision< C >;
Sidebar: ET.Sidebar< C >;
Site: ET.Settings< C >;
Status: ET.PostStatusObject< C >;
Taxonomy: ET.Taxonomy< C >;
Theme: ET.Theme< C >;
UnstableBase: ET.UnstableBase< C >;
User: ET.User< C >;
Widget: ET.Widget< C >;
WidgetType: ET.WidgetType< C >;
};

/**
* A simple utility that pluralizes a string.
* Converts:
* - "post" to "posts"
* - "taxonomy" to "taxonomies"
* - "media" to "mediaItems"
* - "status" to "statuses"
*
* It does not pluralize "GlobalStyles" due to lack of clarity about it at time of writing.
*/
type PluralizeEntity< T extends string > = T extends 'GlobalStyles'
? never
: T extends 'Media'
? 'MediaItems'
: T extends 'Status'
? 'Statuses'
: T extends `${ infer U }y`
? `${ U }ies`
: `${ T }s`;

/**
* A simple utility that singularizes a string.
*
* Converts:
* - "posts" to "post"
* - "taxonomies" to "taxonomy"
* - "mediaItems" to "media"
* - "statuses" to "status"
*/
type SingularizeEntity< T extends string > = T extends 'MediaItems'
? 'Media'
: T extends 'Statuses'
? 'Status'
: T extends `${ infer U }ies`
? `${ U }y`
: T extends `${ infer U }s`
? U
: T;

export type SingularGetters = {
[ Key in `get${ keyof WPEntityTypes }` ]: (
state: State,
id: number | string,
query?: GetRecordsHttpQuery
) => WPEntityTypes[ Key extends `get${ infer E }` ? E : never ] | undefined;
};

export type PluralGetters = {
[ Key in `get${ PluralizeEntity< keyof WPEntityTypes > }` ]: (
state: State,
query?: GetRecordsHttpQuery
) => Array<
WPEntityTypes[ Key extends `get${ infer E }`
? SingularizeEntity< E >
: never ]
> | null;
};

type ActionOptions = {
throwOnError?: boolean;
};

type DeleteRecordsHttpQuery = Record< string, any >;

export type SaveActions = {
[ Key in `save${ keyof WPEntityTypes }` ]: (
data: Partial<
WPEntityTypes[ Key extends `save${ infer E }` ? E : never ]
>,
options?: ActionOptions
) => Promise< void >;
};

export type DeleteActions = {
[ Key in `delete${ keyof WPEntityTypes }` ]: (
id: number | string,
query?: DeleteRecordsHttpQuery,
options?: ActionOptions
) => Promise< void >;
};

export let dynamicActions: SaveActions & DeleteActions;

export let dynamicSelectors: SingularGetters & PluralGetters;
84 changes: 84 additions & 0 deletions packages/core-data/src/entity-types/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Internal dependencies
*/
import type { Context, OmitNevers } from './helpers';
import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records';

export type TemplatePartArea = {
area: string;
label: string;
icon: string;
description: string;
};

export type TemplateType = {
title: string;
description: string;
slug: string;
};

declare module './base-entity-records' {
export namespace BaseEntityRecords {
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export interface Base< C extends Context > {
/**
* Site description.
*/
description: string;

/**
* GMT offset for the site.
*/
gmt_offset: string;

/**
* Home URL.
*/
home: string;

/**
* Site title
*/
name: string;

/**
* Site icon ID.
*/
site_icon?: number;

/**
* Site icon URL.
*/
site_icon_url: string;

/**
* Site logo ID.
*/
site_logo?: number;

/**
* Site timezone string.
*/
timezone_string: string;

/**
* Site URL.
*/
url: string;

/**
* Default template part areas.
*/
default_template_part_areas?: Array< TemplatePartArea >;

/**
* Default template types
*/
default_template_types?: Array< TemplateType >;
}
}
}

export type Base< C extends Context = 'edit' > = OmitNevers<
_BaseEntityRecords.Base< C >
>;
10 changes: 9 additions & 1 deletion packages/core-data/src/entity-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import type { Context, Updatable } from './helpers';
import type { Attachment } from './attachment';
import type { Base, TemplatePartArea, TemplateType } from './base';
import type { Comment } from './comment';
import type { GlobalStylesRevision } from './global-styles-revision';
import type { MenuLocation } from './menu-location';
Expand All @@ -11,6 +12,7 @@ import type { NavMenuItem } from './nav-menu-item';
import type { Page } from './page';
import type { Plugin } from './plugin';
import type { Post } from './post';
import type { PostStatusObject } from './post-status';
import type { PostRevision } from './post-revision';
import type { Settings } from './settings';
import type { Sidebar } from './sidebar';
Expand All @@ -27,6 +29,7 @@ export type { BaseEntityRecords } from './base-entity-records';

export type {
Attachment,
Base as UnstableBase,
Comment,
Context,
GlobalStylesRevision,
Expand All @@ -37,13 +40,16 @@ export type {
Plugin,
Post,
PostRevision,
PostStatusObject,
Settings,
Sidebar,
Taxonomy,
TemplatePartArea,
TemplateType,
Theme,
Type,
Updatable,
User,
Type,
Widget,
WidgetType,
WpTemplate,
Expand Down Expand Up @@ -84,6 +90,7 @@ export type {
*/
export interface PerPackageEntityRecords< C extends Context > {
core:
| Base< C >
| Attachment< C >
| Comment< C >
| GlobalStylesRevision< C >
Expand All @@ -93,6 +100,7 @@ export interface PerPackageEntityRecords< C extends Context > {
| Page< C >
| Plugin< C >
| Post< C >
| PostStatusObject< C >
| PostRevision< C >
| Settings< C >
| Sidebar< C >
Expand Down
56 changes: 56 additions & 0 deletions packages/core-data/src/entity-types/post-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Internal dependencies
*/
import type { Context, OmitNevers } from './helpers';
import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records';

declare module './base-entity-records' {
export namespace BaseEntityRecords {
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export interface PostStatusObject< C extends Context > {
/**
* The title for the status.
*/
name: string;

/**
* Whether posts with this status should be private.
*/
private: boolean;

/**
* Whether posts with this status should be protected.
*/
protected: boolean;

/**
* Whether posts of this status should be shown in the front end of the site.
*/
public: boolean;

/**
* Whether posts with this status should be publicly-queryable.
*/
queryable: boolean;

/**
* Whether to include posts in the edit listing for their post type.
*/
show_in_list: boolean;

/**
* An alphanumeric identifier for the status.
*/
slug: string;

/**
* Whether posts of this status may have floating published dates.
*/
date_floating: boolean;
}
}
}

export type PostStatusObject< C extends Context = 'edit' > = OmitNevers<
_BaseEntityRecords.Type< C >
>;
14 changes: 12 additions & 2 deletions packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from './entities';
import { STORE_NAME } from './name';
import { unlock } from './lock-unlock';
import { dynamicActions, dynamicSelectors } from './dynamic-entities';

// The entity selectors/resolvers and actions are shortcuts to their generic equivalents
// (getEntityRecord, getEntityRecords, updateEntityRecord, updateEntityRecords)
Expand Down Expand Up @@ -68,8 +69,17 @@ const entityActions = entitiesConfig.reduce( ( result, entity ) => {

const storeConfig = () => ( {
reducer,
actions: { ...actions, ...entityActions, ...createLocksActions() },
selectors: { ...selectors, ...entitySelectors },
actions: {
...dynamicActions,
...actions,
...entityActions,
...createLocksActions(),
},
selectors: {
...dynamicSelectors,
...selectors,
...entitySelectors,
},
resolvers: { ...resolvers, ...entityResolvers },
} );

Expand Down
2 changes: 1 addition & 1 deletion packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ type Optional< T > = T | undefined;
/**
* HTTP Query parameters sent with the API request to fetch the entity records.
*/
type GetRecordsHttpQuery = Record< string, any >;
export type GetRecordsHttpQuery = Record< string, any >;

/**
* Arguments for EntityRecord selectors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
__experimentalVStack as VStack,
} from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import type { TemplatePartArea } from '@wordpress/core-data';
import { store as coreStore } from '@wordpress/core-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
Expand Down Expand Up @@ -52,13 +53,6 @@ type CreateTemplatePartModalContentsProps = {
defaultTitle?: string;
};

type TemplatePartArea = {
area: string;
label: string;
icon: string;
description: string;
};

/**
* A React component that renders a modal for creating a template part. The modal displays a title and the contents for creating the template part.
* This component should not live in this package, it should be moved to a dedicated package responsible for managing template.
Expand All @@ -73,7 +67,6 @@ export default function CreateTemplatePartModal( {
} & CreateTemplatePartModalContentsProps ) {
const defaultModalTitle = useSelect(
( select ) =>
// @ts-expect-error getPostType is not typed with 'wp_template_part' as argument.
select( coreStore ).getPostType( 'wp_template_part' )?.labels
?.add_new_item,
[]
Expand Down Expand Up @@ -135,7 +128,6 @@ export function CreateTemplatePartModalContents( {

const defaultTemplatePartAreas = useSelect(
( select ) =>
// @ts-expect-error getEntityRecord is not typed with unstableBase as argument.
select( coreStore ).getEntityRecord< {
default_template_part_areas: Array< TemplatePartArea >;
} >( 'root', '__unstableBase' )?.default_template_part_areas,
Expand Down
Loading

0 comments on commit 72a9996

Please sign in to comment.