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
…#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

1 comment on commit 72a9996

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in 72a9996.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/12626411034
📝 Reported issues:

Please sign in to comment.