diff --git a/docs/reference-guides/view-config-reference.md b/docs/reference-guides/view-config-reference.md index 6bed75d2067272..49e2b69fc49d7a 100644 --- a/docs/reference-guides/view-config-reference.md +++ b/docs/reference-guides/view-config-reference.md @@ -4,6 +4,8 @@ DataViews-powered admin screens (such as the Site Editor's Pages, Templates, Tem The configuration is built per entity — identified by its kind (e.g. `postType`) and name (e.g. `page`) — and can be customized through the dynamic `get_entity_view_config_{$kind}_{$name}` filter, where the dynamic portions are lowercased: the `postType`/`page` entity maps to the `get_entity_view_config_posttype_page` filter. The editor retrieves the result through the `/wp/v2/view-config?kind={kind}&name={name}` REST API endpoint. +The property tables in this document are generated from the REST controller's schema, the single source of truth, by running `npm run docs:view-config-ref`. + Related docs: - [Server-side view configuration filter](/docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md#server-side-view-configuration-filter): how to customize the configuration via the `get_entity_view_config_{$kind}_{$name}` filter and its methods (`merge`, `remove`, `replace`, `set`). @@ -23,13 +25,19 @@ This section lists the properties of the view configuration. It has four top-lev ## default_view -> The view object is the same structure the [DataViews component](/packages/dataviews/README.md#view-object) consumes; see its documentation for the complete list of view properties and filter operators. There are two properties from the DataViews API that cannot be configured via the server filter: `search` and `page`. These are URL-managed that are set by the editor when the user searches or paginates. +> The view object is the same structure the [DataViews component](/packages/dataviews/README.md#view-object) consumes; see its documentation for the complete list of view properties and filter operators. There are two properties from the DataViews API that cannot be configured via the server filter: `search` and `page`. These are URL-managed: they are set by the editor when the user searches or paginates. The default DataViews configuration for the screen: layout type, visible fields, sorting, filtering, and pagination. It is the view users see before they customize anything. + + | Property | Description | Type | | -------- | ----------- | ---- | -| type | The layout type, one of `table`, `grid`, `list`, or `activity`. | `string` | +| type | The layout type (e.g. `table`, `grid`, `list`, or `activity`). | `string` | +| layout | Configuration specific to the selected layout type. Accepts the layout options of all view types. | `{ styles, density, enableMoving, badgeFields, previewSize }` | +| filters | Filters applied to the dataset. A filter with `isLocked` set cannot be removed by the user. | `[ { field, operator, value, isLocked } ]` | +| sort | The sorting configuration: the field id and the direction (`asc` or `desc`). | `{ field, direction }` | +| perPage | Number of records per page. Also used as the batch size when infinite scroll is enabled. | `integer` | | fields | Ids of the fields that are visible, in display order. | `[ string ]` | | titleField | Id of the field used as the record title. | `string` | | mediaField | Id of the field used as the record media (e.g. featured image or preview). | `string` | @@ -38,20 +46,14 @@ The default DataViews configuration for the screen: layout type, visible fields, | showMedia | Whether the media is shown. Defaults to `true`. | `boolean` | | showDescription | Whether the description is shown. Defaults to `true`. | `boolean` | | showLevels | Whether to display hierarchical levels for the records (e.g. child pages indented under their parent). Defaults to `false`. | `boolean` | -| sort | The default sort: the field id and the direction (`asc` or `desc`). | `{ field, direction }` | -| filters | Filters applied to the dataset. A filter with `isLocked` set cannot be removed by the user. | `[ { field, operator, value, isLocked } ]` | -| perPage | Number of records per page. | `integer` | -| layout | Configuration specific to the selected layout type. See [`default_layouts`](#default_layouts). | `object` | +| groupBy | The grouping configuration: the field id, the direction the groups are sorted by (`asc` or `desc`), and whether the group header shows the field label. | `{ field, direction, showLabel }` | +| infiniteScrollEnabled | Whether records load via infinite scroll instead of pagination. | `boolean` | + + ## default_layouts -The layout types the user can switch between. Each key is a layout type (`table`, `grid`, `list`, `activity`); a type that is not present is not offered in the UI. Each value is a partial view configuration applied when the user switches to that layout type — an empty array means the type is available with no overrides. - -| Property | Description | Type | -| -------- | ----------- | ---- | -| table | View overrides applied when the table layout is selected. | `object` | -| grid | View overrides applied when the grid layout is selected. | `object` | -| list | View overrides applied when the list layout is selected. | `object` | +The layout types the user can switch between. Each key is a layout type; a type that is not present is not offered in the UI. Each value is a partial view configuration applied when the user switches to that layout type — an empty array means the type is available with no overrides. Common overrides are `layout` (layout-specific configuration, such as per-field column `styles` for tables or `badgeFields` for grids) and view properties like `showMedia`: @@ -74,15 +76,90 @@ $default_layouts = array( ); ``` + + +| Property | Description | Type | +| -------- | ----------- | ---- | +| table | View overrides applied when the table layout is selected. | `object` | +| list | View overrides applied when the list layout is selected. | `object` | +| grid | View overrides applied when the grid layout is selected. | `object` | +| activity | View overrides applied when the activity layout is selected. | `object` | +| pickerGrid | View overrides applied when the picker grid layout is selected. | `object` | +| pickerTable | View overrides applied when the picker table layout is selected. | `object` | + +Each layout type accepts all the [view properties](#default_view) as overrides (except `type`), plus a `layout` object with options specific to that layout type: + +### table + +Layout options specific to table-type views. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| styles | Styles for the table columns, keyed by field id. | `{ [ fieldId ]: { width, maxWidth, minWidth, align } }` | +| density | The density of the layout. | `compact \| balanced \| comfortable` | +| enableMoving | Whether the table columns display moving controls. | `boolean` | + +### list + +Layout options specific to list-type views. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| density | The density of the layout. | `compact \| balanced \| comfortable` | + +### grid + +Layout options specific to grid-type views. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| badgeFields | Ids of the fields rendered without label and styled as badges. | `[ string ]` | +| previewSize | The size of the grid item preview. | `number` | +| density | The density of the layout. | `compact \| balanced \| comfortable` | + +### activity + +Layout options specific to list-type views. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| density | The density of the layout. | `compact \| balanced \| comfortable` | + +### pickerGrid + +Layout options specific to grid-type views. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| badgeFields | Ids of the fields rendered without label and styled as badges. | `[ string ]` | +| previewSize | The size of the grid item preview. | `number` | +| density | The density of the layout. | `compact \| balanced \| comfortable` | + +### pickerTable + +Layout options specific to table-type views. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| styles | Styles for the table columns, keyed by field id. | `{ [ fieldId ]: { width, maxWidth, minWidth, align } }` | +| density | The density of the layout. | `compact \| balanced \| comfortable` | +| enableMoving | Whether the table columns display moving controls. | `boolean` | + + + ## view_list The preconfigured views displayed in the screen's sidebar (e.g. "All pages", "Published", "Drafts"). Each entry is: + + | Property | Description | Type | | -------- | ----------- | ---- | | title | Title of the view, displayed in the sidebar. | `string` | | slug | Unique identifier for the view. Used as the member identity when merging patches. | `string` | -| view | Partial view configuration applied on top of `default_view` when the view is selected — typically locked `filters`, but any view property works. Optional. | `object` | +| view | Partial view configuration applied on top of the default view when the view is selected. | `object` | + + ```php $view_list = array( @@ -113,20 +190,6 @@ $view_list = array( The DataForm configuration for the Quick Edit form: which fields are displayed, in which order, and how each one is laid out. -| Property | Description | Type | -| -------- | ----------- | ---- | -| layout | The default layout for the form fields, e.g. `{ 'type' => 'panel' }`. | `{ type }` | -| fields | The fields of the form, in display order. Each entry is a field id, or an object for further configuration (see below). | `[ string \| object ]` | - -A field declared as an object accepts: - -| Property | Description | Type | -| -------- | ----------- | ---- | -| id | Id of the field. Used as the member identity when merging patches. | `string` | -| label | Label displayed for the field, overriding the field's own. | `string` | -| layout | How the field is rendered: `type` is one of `regular`, `panel`, `card`, or `row`, and `labelPosition` is one of `side`, `top`, or `none`. | `{ type, labelPosition }` | -| children | Fields combined under this entry, following the same shape as `fields`. | `[ string \| object ]` | - Only registered fields are rendered: a form entry whose field is not registered for the entity is dropped by the editor. ```php @@ -150,3 +213,80 @@ $form = array( ), ); ``` + + + +| Property | Description | Type | +| -------- | ----------- | ---- | +| layout | The default layout for the form fields. | `{ type: regular \| panel \| card \| row \| details, … }` | +| fields | The fields of the form, in display order. Each entry is a field id, or an object for further configuration. | `[ string \| { id, label, description, layout, children } ]` | + +### Form fields + +A field declared as an object accepts: + +| Property | Description | Type | +| -------- | ----------- | ---- | +| id | Id of the field. Used as the member identity when merging patches. | `string` | +| label | Label displayed for the field, overriding the field's own. | `string` | +| description | A description of the form field's purpose, used to provide additional context. | `string` | +| layout | The layout used to render this field, overriding the form layout. | `{ type: regular \| panel \| card \| row \| details, … }` | +| children | Fields combined under this entry, following the same shape as `fields`. | `[ string \| object ]` | + +### Form layout variants + +A form layout — the form `layout` or a field `layout` — is one of the following variants, discriminated by its `type` property: + +#### regular + +The `regular` layout renders the fields inline. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| type | The layout type. | `regular` | +| labelPosition | Position of the field label. Defaults to `top`. | `top \| side \| none` | + +#### panel + +The `panel` layout renders each field as a button that opens a dropdown or modal editor. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| type | The layout type. | `panel` | +| labelPosition | Position of the field label. Defaults to `side`. | `top \| side \| none` | +| openAs | Whether the panel editor opens as a dropdown or a modal. The object form also configures the labels of the modal buttons. Defaults to `dropdown`. | `dropdown \| modal \| { type, applyLabel, cancelLabel }` | +| summary | Id or ids of the fields displayed in the panel header. | `string \| [ string ]` | +| editVisibility | When the edit controls are visible. Defaults to `on-hover`. | `always \| on-hover` | + +#### card + +The `card` layout renders the fields inside a card, optionally collapsible behind a header. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| type | The layout type. | `card` | +| withHeader | Whether the card renders a header. Defaults to `true`. | `boolean` | +| isOpened | Whether the card is expanded. Defaults to `true`. | `boolean` | +| isCollapsible | Whether the card can be collapsed. Defaults to `true`. | `boolean` | +| summary | Id or ids of the fields displayed in the card header. Object entries control the visibility of each summary field (`always` or `when-collapsed`). | `string \| [ string \| { id, visibility } ]` | + +#### row + +The `row` layout renders the fields horizontally in a single row. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| type | The layout type. | `row` | +| alignment | Alignment of the fields within the row. Defaults to `center`. | `start \| center \| end` | +| styles | Styles for the fields in the row, keyed by field id. The `flex` property of each entry controls how the field sizes within the row. | `{ [ fieldId ]: { flex } }` | + +#### details + +The `details` layout renders the fields inside a collapsible details element. + +| Property | Description | Type | +| -------- | ----------- | ---- | +| type | The layout type. | `details` | +| summary | Text shown in the details disclosure summary. | `string` | + + diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index f625e357b65da4..004d46c30ea1c8 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -227,585 +227,17 @@ protected function cast_empty_objects( $value, $schema ) { /** * Retrieves the item's schema, conforming to JSON Schema. * + * The schema lives in a standalone file so that it can also be consumed + * outside of a WordPress runtime by the documentation generator + * (`npm run docs:view-config-ref`). + * * @return array Item schema data. */ public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); + if ( ! $this->schema ) { + $this->schema = require __DIR__ . '/view-config-schema.php'; } - $view_base_properties = $this->get_view_base_schema(); - - $this->schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'view-config', - 'type' => 'object', - 'properties' => array( - 'kind' => array( - 'description' => __( 'Entity kind.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Entity name.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - ), - 'version' => array( - 'description' => __( 'The schema version of the configuration.', 'gutenberg' ), - 'type' => 'integer', - 'readonly' => true, - ), - 'default_view' => array( - 'description' => __( 'Default view configuration.', 'gutenberg' ), - 'type' => 'object', - 'readonly' => true, - 'properties' => array_merge( - array( - 'type' => array( - 'type' => 'string', - ), - 'layout' => $this->get_combined_layout_schema(), - ), - $view_base_properties - ), - ), - 'default_layouts' => array( - 'description' => __( 'Default layout configurations.', 'gutenberg' ), - 'type' => 'object', - 'readonly' => true, - 'properties' => array( - 'table' => array( - 'type' => 'object', - 'properties' => array_merge( - $view_base_properties, - array( - 'layout' => $this->get_table_layout_schema(), - ) - ), - ), - 'list' => array( - 'type' => 'object', - 'properties' => array_merge( - $view_base_properties, - array( - 'layout' => $this->get_list_layout_schema(), - ) - ), - ), - 'grid' => array( - 'type' => 'object', - 'properties' => array_merge( - $view_base_properties, - array( - 'layout' => $this->get_grid_layout_schema(), - ) - ), - ), - 'activity' => array( - 'type' => 'object', - 'properties' => array_merge( - $view_base_properties, - array( - 'layout' => $this->get_list_layout_schema(), - ) - ), - ), - 'pickerGrid' => array( - 'type' => 'object', - 'properties' => array_merge( - $view_base_properties, - array( - 'layout' => $this->get_grid_layout_schema(), - ) - ), - ), - 'pickerTable' => array( - 'type' => 'object', - 'properties' => array_merge( - $view_base_properties, - array( - 'layout' => $this->get_table_layout_schema(), - ) - ), - ), - ), - ), - 'view_list' => array( - 'description' => __( 'List of default views.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'title' => array( - 'type' => 'string', - ), - 'slug' => array( - 'type' => 'string', - ), - 'view' => array( - 'type' => 'object', - 'properties' => array_merge( - array( - 'type' => array( - 'type' => 'string', - ), - 'layout' => $this->get_combined_layout_schema(), - ), - $view_base_properties - ), - ), - ), - ), - ), - 'form' => array( - 'description' => __( 'Default form configuration.', 'gutenberg' ), - 'type' => 'object', - 'readonly' => true, - 'properties' => $this->get_form_schema(), - ), - ), - ); - return $this->add_additional_fields_schema( $this->schema ); } - - /** - * Returns the schema properties shared by all view types (ViewBase), excluding 'type'. - * - * Note that `search` and `page` are not part of the schema: they are managed - * via the URL, which is their only source of truth. - * - * @return array Schema properties for the base view configuration. - */ - protected function get_view_base_schema() { - return array( - 'filters' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'field' => array( - 'type' => 'string', - ), - 'operator' => array( - 'type' => 'string', - 'enum' => array( - 'is', - 'isNot', - 'isAny', - 'isNone', - 'isAll', - 'isNotAll', - 'lessThan', - 'greaterThan', - 'lessThanOrEqual', - 'greaterThanOrEqual', - 'before', - 'after', - ), - ), - 'value' => array(), - 'isLocked' => array( - 'type' => 'boolean', - ), - ), - ), - ), - 'sort' => array( - 'type' => 'object', - 'properties' => array( - 'field' => array( - 'type' => 'string', - ), - 'direction' => array( - 'type' => 'string', - 'enum' => array( 'asc', 'desc' ), - ), - ), - ), - 'perPage' => array( - 'type' => 'integer', - ), - 'fields' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), - 'titleField' => array( - 'type' => 'string', - ), - 'mediaField' => array( - 'type' => 'string', - ), - 'descriptionField' => array( - 'type' => 'string', - ), - 'showTitle' => array( - 'type' => 'boolean', - ), - 'showMedia' => array( - 'type' => 'boolean', - ), - 'showDescription' => array( - 'type' => 'boolean', - ), - 'showLevels' => array( - 'type' => 'boolean', - ), - 'groupBy' => array( - 'type' => 'object', - 'properties' => array( - 'field' => array( - 'type' => 'string', - ), - 'direction' => array( - 'type' => 'string', - 'enum' => array( 'asc', 'desc' ), - ), - 'showLabel' => array( - 'type' => 'boolean', - 'default' => true, - ), - ), - ), - 'infiniteScrollEnabled' => array( - 'type' => 'boolean', - ), - ); - } - - /** - * Returns the schema for the ColumnStyle type. - * - * @return array Schema for a column style object. - */ - protected function get_column_style_schema() { - return array( - 'type' => 'object', - 'properties' => array( - 'width' => array( - 'type' => array( 'string', 'number' ), - ), - 'maxWidth' => array( - 'type' => array( 'string', 'number' ), - ), - 'minWidth' => array( - 'type' => array( 'string', 'number' ), - ), - 'align' => array( - 'type' => 'string', - 'enum' => array( 'start', 'center', 'end' ), - ), - ), - ); - } - - /** - * Returns the layout schema for table-type views (ViewTable, ViewPickerTable). - * - * @return array Schema for a table layout object. - */ - protected function get_table_layout_schema() { - return array( - 'type' => 'object', - 'properties' => array( - 'styles' => array( - 'type' => 'object', - 'additionalProperties' => $this->get_column_style_schema(), - ), - 'density' => array( - 'type' => 'string', - 'enum' => array( 'compact', 'balanced', 'comfortable' ), - ), - 'enableMoving' => array( - 'type' => 'boolean', - ), - ), - ); - } - - /** - * Returns the layout schema for list-type views (ViewList, ViewActivity). - * - * @return array Schema for a list layout object. - */ - protected function get_list_layout_schema() { - return array( - 'type' => 'object', - 'properties' => array( - 'density' => array( - 'type' => 'string', - 'enum' => array( 'compact', 'balanced', 'comfortable' ), - ), - ), - ); - } - - /** - * Returns a combined layout schema that accepts properties from all view types. - * - * This is useful for contexts where the view type is not known ahead of time - * (e.g. the `view` override in a view list item), so all possible layout - * properties must be accepted. - * - * @return array Schema for a combined layout object. - */ - protected function get_combined_layout_schema() { - return array( - 'type' => 'object', - 'properties' => array_merge( - $this->get_table_layout_schema()['properties'], - $this->get_grid_layout_schema()['properties'], - $this->get_list_layout_schema()['properties'] - ), - ); - } - - /** - * Returns the layout schema for grid-type views (ViewGrid, ViewPickerGrid). - * - * @return array Schema for a grid layout object. - */ - protected function get_grid_layout_schema() { - return array( - 'type' => 'object', - 'properties' => array( - 'badgeFields' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), - 'previewSize' => array( - 'type' => 'number', - ), - 'density' => array( - 'type' => 'string', - 'enum' => array( 'compact', 'balanced', 'comfortable' ), - ), - ), - ); - } - - /** - * Returns the schema for a form layout object as a discriminated union. - * - * Each variant is discriminated by a single-value enum on its `type` property, - * matching the TypeScript Layout union in dataviews/src/types/dataform.ts. - * - * @return array Schema for a form layout object. - */ - protected function get_form_layout_schema() { - return array( - 'oneOf' => array( - // RegularLayout. - array( - 'type' => 'object', - 'properties' => array( - 'type' => array( - 'type' => 'string', - 'enum' => array( 'regular' ), - ), - 'labelPosition' => array( - 'type' => 'string', - 'enum' => array( 'top', 'side', 'none' ), - ), - ), - ), - // PanelLayout. - array( - 'type' => 'object', - 'properties' => array( - 'type' => array( - 'type' => 'string', - 'enum' => array( 'panel' ), - ), - 'labelPosition' => array( - 'type' => 'string', - 'enum' => array( 'top', 'side', 'none' ), - ), - 'openAs' => array( - 'oneOf' => array( - array( - 'type' => 'string', - 'enum' => array( 'dropdown', 'modal' ), - ), - array( - 'type' => 'object', - 'properties' => array( - 'type' => array( - 'type' => 'string', - 'enum' => array( 'dropdown', 'modal' ), - ), - 'applyLabel' => array( - 'type' => 'string', - ), - 'cancelLabel' => array( - 'type' => 'string', - ), - ), - ), - ), - ), - 'summary' => array( - 'oneOf' => array( - array( 'type' => 'string' ), - array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), - ), - ), - 'editVisibility' => array( - 'type' => 'string', - 'enum' => array( 'always', 'on-hover' ), - ), - ), - ), - // CardLayout. - array( - 'type' => 'object', - 'properties' => array( - 'type' => array( - 'type' => 'string', - 'enum' => array( 'card' ), - ), - 'withHeader' => array( - 'type' => 'boolean', - ), - 'isOpened' => array( - 'type' => 'boolean', - ), - 'isCollapsible' => array( - 'type' => 'boolean', - ), - 'summary' => array( - 'oneOf' => array( - array( 'type' => 'string' ), - array( - 'type' => 'array', - 'items' => array( - 'oneOf' => array( - array( 'type' => 'string' ), - array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'type' => 'string', - ), - 'visibility' => array( - 'type' => 'string', - 'enum' => array( 'always', 'when-collapsed' ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - // RowLayout. - array( - 'type' => 'object', - 'properties' => array( - 'type' => array( - 'type' => 'string', - 'enum' => array( 'row' ), - ), - 'alignment' => array( - 'type' => 'string', - 'enum' => array( 'start', 'center', 'end' ), - ), - 'styles' => array( - 'type' => 'object', - 'additionalProperties' => array( - 'type' => 'object', - 'properties' => array( - 'flex' => array( - 'type' => array( 'string', 'number' ), - ), - ), - ), - ), - ), - ), - // DetailsLayout. - array( - 'type' => 'object', - 'properties' => array( - 'type' => array( - 'type' => 'string', - 'enum' => array( 'details' ), - ), - 'summary' => array( - 'type' => 'string', - ), - ), - ), - ), - ); - } - - /** - * Returns the schema for a form field item (string or object). - * - * @return array Schema for a form field. - */ - protected function get_form_field_schema() { - return array( - 'oneOf' => array( - array( 'type' => 'string' ), - array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'type' => 'string', - ), - 'label' => array( - 'type' => 'string', - ), - 'description' => array( - 'type' => 'string', - ), - 'layout' => $this->get_form_layout_schema(), - 'children' => array( - 'type' => 'array', - 'items' => array( - 'oneOf' => array( - array( 'type' => 'string' ), - // This object can have the shape of a form field itself, - // allowing for recursive nesting of form fields. - // There's no easy way to codify this recursion via the JSON Schema draft-04 - // supported by the REST API. - array( 'type' => 'object' ), - ), - ), - ), - ), - ), - ), - ); - } - - /** - * Returns the schema for the form configuration object. - * - * @return array Schema properties for the form configuration. - */ - protected function get_form_schema() { - return array( - 'layout' => $this->get_form_layout_schema(), - 'fields' => array( - 'type' => 'array', - 'items' => $this->get_form_field_schema(), - ), - ); - } } diff --git a/lib/compat/wordpress-7.1/view-config-schema.php b/lib/compat/wordpress-7.1/view-config-schema.php new file mode 100644 index 00000000000000..7a69b0ab09269d --- /dev/null +++ b/lib/compat/wordpress-7.1/view-config-schema.php @@ -0,0 +1,647 @@ + array( + 'description' => __( 'Filters applied to the dataset. A filter with `isLocked` set cannot be removed by the user.', 'gutenberg' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'field' => array( + 'description' => __( 'Id of the field to filter by.', 'gutenberg' ), + 'type' => 'string', + ), + 'operator' => array( + 'description' => __( 'The filter operator to use.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( + 'is', + 'isNot', + 'isAny', + 'isNone', + 'isAll', + 'isNotAll', + 'lessThan', + 'greaterThan', + 'lessThanOrEqual', + 'greaterThanOrEqual', + 'before', + 'after', + ), + ), + 'value' => array( + 'description' => __( 'The value to filter by.', 'gutenberg' ), + ), + 'isLocked' => array( + 'description' => __( 'Whether the filter is locked, preventing the user from editing or removing it.', 'gutenberg' ), + 'type' => 'boolean', + ), + ), + ), + ), + 'sort' => array( + 'description' => __( 'The sorting configuration: the field id and the direction (`asc` or `desc`).', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'field' => array( + 'description' => __( 'Id of the field to sort by.', 'gutenberg' ), + 'type' => 'string', + ), + 'direction' => array( + 'description' => __( 'The direction to sort by.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'asc', 'desc' ), + ), + ), + ), + 'perPage' => array( + 'description' => __( 'Number of records per page. Also used as the batch size when infinite scroll is enabled.', 'gutenberg' ), + 'type' => 'integer', + ), + 'fields' => array( + 'description' => __( 'Ids of the fields that are visible, in display order.', 'gutenberg' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'titleField' => array( + 'description' => __( 'Id of the field used as the record title.', 'gutenberg' ), + 'type' => 'string', + ), + 'mediaField' => array( + 'description' => __( 'Id of the field used as the record media (e.g. featured image or preview).', 'gutenberg' ), + 'type' => 'string', + ), + 'descriptionField' => array( + 'description' => __( 'Id of the field used as the record description.', 'gutenberg' ), + 'type' => 'string', + ), + 'showTitle' => array( + 'description' => __( 'Whether the title is shown. Defaults to `true`.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'showMedia' => array( + 'description' => __( 'Whether the media is shown. Defaults to `true`.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'showDescription' => array( + 'description' => __( 'Whether the description is shown. Defaults to `true`.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'showLevels' => array( + 'description' => __( 'Whether to display hierarchical levels for the records (e.g. child pages indented under their parent). Defaults to `false`.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'groupBy' => array( + 'description' => __( 'The grouping configuration: the field id, the direction the groups are sorted by (`asc` or `desc`), and whether the group header shows the field label.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'field' => array( + 'description' => __( 'Id of the field to group by.', 'gutenberg' ), + 'type' => 'string', + ), + 'direction' => array( + 'description' => __( 'The direction the groups are sorted by.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'asc', 'desc' ), + ), + 'showLabel' => array( + 'description' => __( 'Whether to show the field label in the group header.', 'gutenberg' ), + 'type' => 'boolean', + 'default' => true, + ), + ), + ), + 'infiniteScrollEnabled' => array( + 'description' => __( 'Whether records load via infinite scroll instead of pagination.', 'gutenberg' ), + 'type' => 'boolean', + ), +); + +// Schema for the ColumnStyle type. +$column_style_schema = array( + 'description' => __( 'Style of a table column.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'width' => array( + 'description' => __( 'The width of the column.', 'gutenberg' ), + 'type' => array( 'string', 'number' ), + ), + 'maxWidth' => array( + 'description' => __( 'The maximum width of the column.', 'gutenberg' ), + 'type' => array( 'string', 'number' ), + ), + 'minWidth' => array( + 'description' => __( 'The minimum width of the column.', 'gutenberg' ), + 'type' => array( 'string', 'number' ), + ), + 'align' => array( + 'description' => __( 'The alignment of the column content. Defaults to `start`.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'start', 'center', 'end' ), + ), + ), +); + +// Layout schema for table-type views (ViewTable, ViewPickerTable). +$table_layout_schema = array( + 'description' => __( 'Layout options specific to table-type views.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'styles' => array( + 'description' => __( 'Styles for the table columns, keyed by field id.', 'gutenberg' ), + 'type' => 'object', + 'additionalProperties' => $column_style_schema, + ), + 'density' => array( + 'description' => __( 'The density of the layout.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'compact', 'balanced', 'comfortable' ), + ), + 'enableMoving' => array( + 'description' => __( 'Whether the table columns display moving controls.', 'gutenberg' ), + 'type' => 'boolean', + ), + ), +); + +// Layout schema for list-type views (ViewList, ViewActivity). +$list_layout_schema = array( + 'description' => __( 'Layout options specific to list-type views.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'density' => array( + 'description' => __( 'The density of the layout.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'compact', 'balanced', 'comfortable' ), + ), + ), +); + +// Layout schema for grid-type views (ViewGrid, ViewPickerGrid). +$grid_layout_schema = array( + 'description' => __( 'Layout options specific to grid-type views.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'badgeFields' => array( + 'description' => __( 'Ids of the fields rendered without label and styled as badges.', 'gutenberg' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'previewSize' => array( + 'description' => __( 'The size of the grid item preview.', 'gutenberg' ), + 'type' => 'number', + ), + 'density' => array( + 'description' => __( 'The density of the layout.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'compact', 'balanced', 'comfortable' ), + ), + ), +); + +/* + * A combined layout schema that accepts properties from all view types. + * + * This is useful for contexts where the view type is not known ahead of time + * (e.g. the `view` override in a view list item), so all possible layout + * properties must be accepted. + */ +$combined_layout_schema = array( + 'description' => __( 'Configuration specific to the selected layout type. Accepts the layout options of all view types.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + $table_layout_schema['properties'], + $grid_layout_schema['properties'], + $list_layout_schema['properties'] + ), +); + +/* + * Schema for a form layout object as a discriminated union. + * + * Each variant is discriminated by a single-value enum on its `type` property, + * matching the TypeScript Layout union in dataviews/src/types/dataform.ts. + */ +$form_layout_schema = array( + 'description' => __( 'The layout used to render the fields: one of the `regular`, `panel`, `card`, `row`, or `details` variants, discriminated by its `type` property.', 'gutenberg' ), + 'oneOf' => array( + // RegularLayout. + array( + 'description' => __( 'The `regular` layout renders the fields inline.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'description' => __( 'The layout type.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'regular' ), + ), + 'labelPosition' => array( + 'description' => __( 'Position of the field label. Defaults to `top`.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'top', 'side', 'none' ), + ), + ), + ), + // PanelLayout. + array( + 'description' => __( 'The `panel` layout renders each field as a button that opens a dropdown or modal editor.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'description' => __( 'The layout type.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'panel' ), + ), + 'labelPosition' => array( + 'description' => __( 'Position of the field label. Defaults to `side`.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'top', 'side', 'none' ), + ), + 'openAs' => array( + 'description' => __( 'Whether the panel editor opens as a dropdown or a modal. The object form also configures the labels of the modal buttons. Defaults to `dropdown`.', 'gutenberg' ), + 'oneOf' => array( + array( + 'type' => 'string', + 'enum' => array( 'dropdown', 'modal' ), + ), + array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'description' => __( 'The editor type the panel opens as.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'dropdown', 'modal' ), + ), + 'applyLabel' => array( + 'description' => __( 'Label of the apply button when the panel opens as a modal.', 'gutenberg' ), + 'type' => 'string', + ), + 'cancelLabel' => array( + 'description' => __( 'Label of the cancel button when the panel opens as a modal.', 'gutenberg' ), + 'type' => 'string', + ), + ), + ), + ), + ), + 'summary' => array( + 'description' => __( 'Id or ids of the fields displayed in the panel header.', 'gutenberg' ), + 'oneOf' => array( + array( 'type' => 'string' ), + array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'editVisibility' => array( + 'description' => __( 'When the edit controls are visible. Defaults to `on-hover`.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'always', 'on-hover' ), + ), + ), + ), + // CardLayout. + array( + 'description' => __( 'The `card` layout renders the fields inside a card, optionally collapsible behind a header.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'description' => __( 'The layout type.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'card' ), + ), + 'withHeader' => array( + 'description' => __( 'Whether the card renders a header. Defaults to `true`.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'isOpened' => array( + 'description' => __( 'Whether the card is expanded. Defaults to `true`.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'isCollapsible' => array( + 'description' => __( 'Whether the card can be collapsed. Defaults to `true`.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'summary' => array( + 'description' => __( 'Id or ids of the fields displayed in the card header. Object entries control the visibility of each summary field (`always` or `when-collapsed`).', 'gutenberg' ), + 'oneOf' => array( + array( 'type' => 'string' ), + array( + 'type' => 'array', + 'items' => array( + 'oneOf' => array( + array( 'type' => 'string' ), + array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Id of the summary field.', 'gutenberg' ), + 'type' => 'string', + ), + 'visibility' => array( + 'description' => __( 'When the summary field is shown: always, or only when the card is collapsed. Defaults to `when-collapsed`.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'always', 'when-collapsed' ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + // RowLayout. + array( + 'description' => __( 'The `row` layout renders the fields horizontally in a single row.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'description' => __( 'The layout type.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'row' ), + ), + 'alignment' => array( + 'description' => __( 'Alignment of the fields within the row. Defaults to `center`.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'start', 'center', 'end' ), + ), + 'styles' => array( + 'description' => __( 'Styles for the fields in the row, keyed by field id. The `flex` property of each entry controls how the field sizes within the row.', 'gutenberg' ), + 'type' => 'object', + 'additionalProperties' => array( + 'type' => 'object', + 'properties' => array( + 'flex' => array( + 'description' => __( 'Any valid CSS `flex` value, controlling how the field sizes within the row.', 'gutenberg' ), + 'type' => array( 'string', 'number' ), + ), + ), + ), + ), + ), + ), + // DetailsLayout. + array( + 'description' => __( 'The `details` layout renders the fields inside a collapsible details element.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'description' => __( 'The layout type.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'details' ), + ), + 'summary' => array( + 'description' => __( 'Text shown in the details disclosure summary.', 'gutenberg' ), + 'type' => 'string', + ), + ), + ), + ), +); + +// Schema for a form field item (string or object). +$form_field_schema = array( + 'description' => __( 'A form field: a field id, or an object for further configuration.', 'gutenberg' ), + 'oneOf' => array( + array( + 'description' => __( 'Id of the field.', 'gutenberg' ), + 'type' => 'string', + ), + array( + 'description' => __( 'A form field configuration object.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Id of the field. Used as the member identity when merging patches.', 'gutenberg' ), + 'type' => 'string', + ), + 'label' => array( + 'description' => __( "Label displayed for the field, overriding the field's own.", 'gutenberg' ), + 'type' => 'string', + ), + 'description' => array( + 'description' => __( "A description of the form field's purpose, used to provide additional context.", 'gutenberg' ), + 'type' => 'string', + ), + 'layout' => array_merge( + $form_layout_schema, + array( + 'description' => __( 'The layout used to render this field, overriding the form layout.', 'gutenberg' ), + ) + ), + 'children' => array( + 'description' => __( 'Fields combined under this entry, following the same shape as `fields`.', 'gutenberg' ), + 'type' => 'array', + 'items' => array( + 'oneOf' => array( + array( + 'description' => __( 'Id of the field.', 'gutenberg' ), + 'type' => 'string', + ), + // This object can have the shape of a form field itself, + // allowing for recursive nesting of form fields. + // There's no easy way to codify this recursion via the JSON Schema draft-04 + // supported by the REST API. + array( + 'description' => __( 'A nested form field configuration object.', 'gutenberg' ), + 'type' => 'object', + ), + ), + ), + ), + ), + ), + ), +); + +// Schema properties for the form configuration object. +$form_properties = array( + 'layout' => array_merge( + $form_layout_schema, + array( + 'description' => __( 'The default layout for the form fields.', 'gutenberg' ), + ) + ), + 'fields' => array( + 'description' => __( 'The fields of the form, in display order. Each entry is a field id, or an object for further configuration.', 'gutenberg' ), + 'type' => 'array', + 'items' => $form_field_schema, + ), +); + +return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'view-config', + 'type' => 'object', + 'properties' => array( + 'kind' => array( + 'description' => __( 'Entity kind.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Entity name.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'The schema version of the configuration.', 'gutenberg' ), + 'type' => 'integer', + 'readonly' => true, + ), + 'default_view' => array( + 'description' => __( 'Default view configuration.', 'gutenberg' ), + 'type' => 'object', + 'readonly' => true, + 'properties' => array_merge( + array( + 'type' => array( + 'description' => __( 'The layout type (e.g. `table`, `grid`, `list`, or `activity`).', 'gutenberg' ), + 'type' => 'string', + ), + 'layout' => $combined_layout_schema, + ), + $view_base_properties + ), + ), + 'default_layouts' => array( + 'description' => __( 'Default layout configurations.', 'gutenberg' ), + 'type' => 'object', + 'readonly' => true, + 'properties' => array( + 'table' => array( + 'description' => __( 'View overrides applied when the table layout is selected.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $table_layout_schema, + ) + ), + ), + 'list' => array( + 'description' => __( 'View overrides applied when the list layout is selected.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $list_layout_schema, + ) + ), + ), + 'grid' => array( + 'description' => __( 'View overrides applied when the grid layout is selected.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $grid_layout_schema, + ) + ), + ), + 'activity' => array( + 'description' => __( 'View overrides applied when the activity layout is selected.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $list_layout_schema, + ) + ), + ), + 'pickerGrid' => array( + 'description' => __( 'View overrides applied when the picker grid layout is selected.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $grid_layout_schema, + ) + ), + ), + 'pickerTable' => array( + 'description' => __( 'View overrides applied when the picker table layout is selected.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $table_layout_schema, + ) + ), + ), + ), + ), + 'view_list' => array( + 'description' => __( 'List of default views.', 'gutenberg' ), + 'type' => 'array', + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'title' => array( + 'description' => __( 'Title of the view, displayed in the sidebar.', 'gutenberg' ), + 'type' => 'string', + ), + 'slug' => array( + 'description' => __( 'Unique identifier for the view. Used as the member identity when merging patches.', 'gutenberg' ), + 'type' => 'string', + ), + 'view' => array( + 'description' => __( 'Partial view configuration applied on top of the default view when the view is selected.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array_merge( + array( + 'type' => array( + 'description' => __( 'The layout type (e.g. `table`, `grid`, `list`, or `activity`).', 'gutenberg' ), + 'type' => 'string', + ), + 'layout' => $combined_layout_schema, + ), + $view_base_properties + ), + ), + ), + ), + ), + 'form' => array( + 'description' => __( 'Default form configuration.', 'gutenberg' ), + 'type' => 'object', + 'readonly' => true, + 'properties' => $form_properties, + ), + ), +); diff --git a/package.json b/package.json index 6ca80fff270e6d..d261a8f2cb2835 100644 --- a/package.json +++ b/package.json @@ -108,11 +108,12 @@ "docs:api-ref": "npm run --workspace @wordpress/docs-tools docs:api-ref --", "docs:blocks": "npm run --workspace @wordpress/docs-tools docs:blocks --", "docs:blocks-detail": "npm run --workspace @wordpress/docs-tools docs:blocks-detail --", - "docs:build": "npm-run-all docs:components docs:gen docs:blocks docs:blocks-detail docs:api-ref docs:theme-ref", + "docs:build": "npm-run-all docs:components docs:gen docs:blocks docs:blocks-detail docs:api-ref docs:theme-ref docs:view-config-ref", "docs:check-api-docs-unstaged": "npm run --workspace @wordpress/docs-tools docs:check-api-docs-unstaged --", "docs:components": "npm run --workspace @wordpress/docs-tools docs:components --", "docs:gen": "npm run --workspace @wordpress/docs-tools docs:gen --", "docs:theme-ref": "npm run --workspace @wordpress/docs-tools docs:theme-ref --", + "docs:view-config-ref": "npm run --workspace @wordpress/docs-tools docs:view-config-ref --", "env": "wp-env", "fixtures:clean": "npm run --workspace @wordpress/integration-tests fixtures:clean --", "fixtures:generate": "cross-env GENERATE_MISSING_FIXTURES=y npm run test:unit -- test/integration/full-content/ && npm run format test/integration/fixtures/blocks/*.json", diff --git a/tools/docs/dump-view-config-schema.php b/tools/docs/dump-view-config-schema.php new file mode 100644 index 00000000000000..442cd8ee24b48a --- /dev/null +++ b/tools/docs/dump-view-config-schema.php @@ -0,0 +1,73 @@ +getMessage() . PHP_EOL . + 'The schema file must stay loadable outside WordPress — no WordPress API other than `__()`.' . PHP_EOL + ); + exit( 1 ); +} diff --git a/tools/docs/gen-view-config-reference.mjs b/tools/docs/gen-view-config-reference.mjs new file mode 100644 index 00000000000000..fed141ab8c2b00 --- /dev/null +++ b/tools/docs/gen-view-config-reference.mjs @@ -0,0 +1,336 @@ +/** + * Generates the view config reference documentation from the view config REST + * schema — the single source of truth, also consumed by the REST controller. + * + * Reads from : lib/compat/wordpress-7.1/view-config-schema.php + * (extracted via tools/docs/dump-view-config-schema.php) + * Publishes to: docs/reference-guides/view-config-reference.md + */ + +/** + * External dependencies + */ +import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import execa from 'execa'; + +/** + * Path to the PHP script that dumps the view config schema as JSON. + * + * @type {string} + */ +const DUMP_SCRIPT_PATH = fileURLToPath( + new URL( './dump-view-config-schema.php', import.meta.url ) +); + +/** + * Path to docs file. + * + * @type {string} + */ +const REFERENCE_DOC_PATH = fileURLToPath( + new URL( + '../../docs/reference-guides/view-config-reference.md', + import.meta.url + ) +); + +/** + * Builds the start token delimiting a generated section in the doc file. + * + * @param {string} section Top-level schema key the section documents. + * @return {string} Start token. + */ +const startToken = ( section ) => + ``; + +/** + * Builds the end token delimiting a generated section in the doc file. + * + * @param {string} section Top-level schema key the section documents. + * @return {string} End token. + */ +const endToken = ( section ) => + ``; + +/** + * Maximum number of keys included in a `{ key, … }` object summary before + * falling back to a plain `object` label. + * + * @type {number} + */ +const OBJECT_SUMMARY_MAX_KEYS = 8; + +/** + * Maximum number of enum values rendered as a `a | b | c` union in the Type + * column before falling back to the primitive type label. + * + * @type {number} + */ +const ENUM_SUMMARY_MAX_VALUES = 6; + +/** + * Serializes a schema node into a compact human-readable type label. + * + * Objects render as `{ key1, key2 }` summaries, maps (via + * `additionalProperties`) as `{ [ key ]: … }`, arrays as `[ itemType ]`, and + * small unions (`oneOf`/`anyOf`) join their branches with `|`. A union of + * three or more object branches discriminated by a single-value `type` enum + * renders as `{ type: a | b | c, … }`. + * + * @param {Object} schema Schema node. + * @return {string} Serialized type, without backticks or escaping. + */ +function formatType( schema ) { + if ( ! schema || typeof schema !== 'object' ) { + return 'any'; + } + + const branches = schema.oneOf || schema.anyOf; + if ( branches ) { + const isDiscriminatedObjectUnion = + branches.length >= 3 && + branches.every( + ( branch ) => + branch.type === 'object' && + branch.properties?.type?.enum?.length === 1 + ); + if ( isDiscriminatedObjectUnion ) { + const variants = branches.map( + ( branch ) => branch.properties.type.enum[ 0 ] + ); + return `{ type: ${ variants.join( ' | ' ) }, … }`; + } + const formatted = branches.map( formatType ).filter( Boolean ); + return [ ...new Set( formatted ) ].join( ' | ' ); + } + + if ( Array.isArray( schema.type ) ) { + return schema.type.join( ' | ' ); + } + + if ( schema.enum && schema.enum.length <= ENUM_SUMMARY_MAX_VALUES ) { + return schema.enum.join( ' | ' ); + } + + if ( schema.type === 'object' ) { + if ( schema.properties ) { + const keys = Object.keys( schema.properties ); + if ( keys.length <= OBJECT_SUMMARY_MAX_KEYS ) { + return `{ ${ keys.join( ', ' ) } }`; + } + return 'object'; + } + if ( schema.additionalProperties?.type ) { + return `{ [ fieldId ]: ${ formatType( + schema.additionalProperties + ) } }`; + } + return 'object'; + } + + if ( schema.type === 'array' ) { + if ( schema.items ) { + return `[ ${ formatType( schema.items ) } ]`; + } + return 'array'; + } + + return schema.type || 'any'; +} + +/** + * Wraps a serialized type in backticks and escapes pipes for use in a + * markdown table cell. + * + * @param {Object} schema Schema node. + * @return {string} Markdown table cell content. + */ +function formatTypeCell( schema ) { + return `\`${ formatType( schema ) }\``.replace( /\|/g, '\\|' ); +} + +/** + * Renders a markdown table for a map of property schemas. A Default column is + * added only when at least one property declares a default. + * + * @param {Object} properties Map of property name to schema node. + * @return {string} Markdown table. + */ +function renderTable( properties ) { + const entries = Object.entries( properties ); + const hasDefaults = entries.some( ( [ , schema ] ) => 'default' in schema ); + + let md = hasDefaults + ? '| Property | Description | Type | Default |\n| -------- | ----------- | ---- | ------- |\n' + : '| Property | Description | Type |\n| -------- | ----------- | ---- |\n'; + + for ( const [ property, schema ] of entries ) { + // Assuming that the first line of a description is a summary. + const description = schema.description?.split( '\n', 1 )[ 0 ] ?? ''; + const cells = [ property, description, formatTypeCell( schema ) ]; + if ( hasDefaults ) { + cells.push( + 'default' in schema + ? `\`${ JSON.stringify( schema.default ) }\`` + : '' + ); + } + md += `| ${ cells.join( ' | ' ) } |\n`; + } + + return md; +} + +/** + * Renders the `default_view` section: a single table of view properties. + * + * @param {Object} schema The item schema. + * @return {string} Markdown content. + */ +function renderDefaultView( schema ) { + return renderTable( schema.properties.default_view.properties ); +} + +/** + * Renders the `default_layouts` section: a summary table of the layout types, + * followed by one subsection per type listing its layout-specific options. + * + * @param {Object} schema The item schema. + * @return {string} Markdown content. + */ +function renderDefaultLayouts( schema ) { + const layoutTypes = schema.properties.default_layouts.properties; + + let md = renderTable( layoutTypes ); + md += '\n'; + md += + 'Each layout type accepts all the [view properties](#default_view) as overrides (except `type`), plus a `layout` object with options specific to that layout type:\n'; + + for ( const [ layoutType, layoutTypeSchema ] of Object.entries( + layoutTypes + ) ) { + const layout = layoutTypeSchema.properties.layout; + md += `\n### ${ layoutType }\n\n`; + if ( layout.description ) { + md += `${ layout.description }\n\n`; + } + md += renderTable( layout.properties ); + } + + return md; +} + +/** + * Renders the `view_list` section: a table of the properties of each entry. + * + * @param {Object} schema The item schema. + * @return {string} Markdown content. + */ +function renderViewList( schema ) { + return renderTable( schema.properties.view_list.items.properties ); +} + +/** + * Renders the `form` section: the top-level form table, the form field object + * table, and one subsection per form layout variant. + * + * @param {Object} schema The item schema. + * @return {string} Markdown content. + */ +function renderForm( schema ) { + const form = schema.properties.form; + + let md = renderTable( form.properties ); + + // Form field objects. + const fieldObjectBranch = form.properties.fields.items.oneOf.find( + ( branch ) => branch.type === 'object' + ); + md += '\n### Form fields\n\n'; + md += 'A field declared as an object accepts:\n\n'; + md += renderTable( fieldObjectBranch.properties ); + + // Form layout variants. + md += '\n### Form layout variants\n\n'; + md += + 'A form layout — the form `layout` or a field `layout` — is one of the following variants, discriminated by its `type` property:\n'; + for ( const branch of form.properties.layout.oneOf ) { + const variant = branch.properties.type.enum[ 0 ]; + md += `\n#### ${ variant }\n\n`; + if ( branch.description ) { + md += `${ branch.description }\n\n`; + } + md += renderTable( branch.properties ); + } + + return md; +} + +/** + * Splices generated content into the doc between the section's tokens. + * + * @param {string} doc Current doc contents. + * @param {string} section Top-level schema key of the section. + * @param {string} content Generated markdown for the section. + * @return {string} Updated doc contents. + */ +function spliceSection( doc, section, content ) { + const start = startToken( section ); + const end = endToken( section ); + const startIndex = doc.indexOf( start ); + const endIndex = doc.indexOf( end ); + + if ( startIndex === -1 || endIndex === -1 || endIndex < startIndex ) { + throw new Error( + `Could not find the "${ start }" / "${ end }" token pair in ${ REFERENCE_DOC_PATH }.` + ); + } + + return ( + doc.slice( 0, startIndex + start.length ) + + '\n\n' + + content + + '\n' + + doc.slice( endIndex ) + ); +} + +/** + * Main function. + */ +async function main() { + let stdout; + try { + ( { stdout } = await execa( 'php', [ DUMP_SCRIPT_PATH ] ) ); + } catch ( error ) { + if ( error.code === 'ENOENT' ) { + throw new Error( + 'PHP is required to generate the view config reference: the schema is extracted from its PHP source file via `php`. Install PHP and make sure it is available on your PATH.' + ); + } + throw error; + } + + const schema = JSON.parse( stdout ); + + const sections = { + default_view: renderDefaultView, + default_layouts: renderDefaultLayouts, + view_list: renderViewList, + form: renderForm, + }; + + let doc = await fs.readFile( REFERENCE_DOC_PATH, { encoding: 'utf8' } ); + + for ( const [ section, render ] of Object.entries( sections ) ) { + doc = spliceSection( doc, section, render( schema ) ); + } + + await fs.writeFile( REFERENCE_DOC_PATH, doc, { encoding: 'utf8' } ); +} + +main().catch( ( error ) => { + console.error( error ); + process.exit( 1 ); +} ); diff --git a/tools/docs/package.json b/tools/docs/package.json index e750a62d109e03..15663a72ac370a 100644 --- a/tools/docs/package.json +++ b/tools/docs/package.json @@ -43,6 +43,7 @@ "docs:blocks-detail": "node ./generate-block-docs.mjs", "docs:check-api-docs-unstaged": "node ./are-api-docs-unstaged.js", "docs:components": "node ./gen-components-docs/index.mjs", - "docs:theme-ref": "node ./gen-theme-reference.mjs" + "docs:theme-ref": "node ./gen-theme-reference.mjs", + "docs:view-config-ref": "node ./gen-view-config-reference.mjs" } }