Skip to content

Commit

Permalink
Visualize hierarchical data in DataViews (#66479)
Browse files Browse the repository at this point in the history
Co-authored-by: oandregal <[email protected]>
Co-authored-by: mcsf <[email protected]>
Co-authored-by: ntsekouras <[email protected]>
Co-authored-by: jameskoster <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: jasmussen <[email protected]>
Co-authored-by: jarekmorawski <[email protected]>
Co-authored-by: ciampo <[email protected]>
  • Loading branch information
9 people authored Dec 18, 2024
1 parent 3159fa2 commit 2e3e6e4
Show file tree
Hide file tree
Showing 17 changed files with 478 additions and 6 deletions.
3 changes: 3 additions & 0 deletions backport-changelog/6.8/8014.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/8014

* https://github.com/WordPress/gutenberg/pull/66479
205 changes: 205 additions & 0 deletions lib/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

/**
* Modifies the Post controller endpoint to support orderby_hierarchy.
*
* @package gutenberg
* @since 6.8.0
*/

class Gutenberg_Hierarchical_Sort {
private static $post_ids = array();
private static $levels = array();
private static $instance;

public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}

return self::$instance;
}

public function run( $args ) {
$new_args = array_merge(
$args,
array(
'fields' => 'id=>parent',
'posts_per_page' => -1,
)
);
$query = new WP_Query( $new_args );
$posts = $query->posts;
$result = self::sort( $posts );

self::$post_ids = $result['post_ids'];
self::$levels = $result['levels'];
}

/**
* Check if the request is eligible for hierarchical sorting.
*
* @param array $request The request data.
*
* @return bool Return true if the request is eligible for hierarchical sorting.
*/
public static function is_eligible( $request ) {
if ( ! isset( $request['orderby_hierarchy'] ) || true !== $request['orderby_hierarchy'] ) {
return false;
}

return true;
}

public static function get_ancestor( $post_id ) {
return get_post( $post_id )->post_parent ?? 0;
}

/**
* Sort posts by hierarchy.
*
* Takes an array of posts and sorts them based on their parent-child relationships.
* It also tracks the level depth of each post in the hierarchy.
*
* Example input:
* ```
* [
* ['ID' => 4, 'post_parent' => 2],
* ['ID' => 2, 'post_parent' => 0],
* ['ID' => 3, 'post_parent' => 2],
* ]
* ```
*
* Example output:
* ```
* [
* 'post_ids' => [2, 4, 3],
* 'levels' => [0, 1, 1]
* ]
* ```
*
* @param array $posts Array of post objects containing ID and post_parent properties.
*
* @return array {
* Sorted post IDs and their hierarchical levels
*
* @type array $post_ids Array of post IDs
* @type array $levels Array of levels for the corresponding post ID in the same index
* }
*/
public static function sort( $posts ) {
/*
* Arrange pages in two arrays:
*
* - $top_level: posts whose parent is 0
* - $children: post ID as the key and an array of children post IDs as the value.
* Example: $children[10][] contains all sub-pages whose parent is 10.
*
* Additionally, keep track of the levels of each post in $levels.
* Example: $levels[10] = 0 means the post ID is a top-level page.
*
*/
$top_level = array();
$children = array();
foreach ( $posts as $post ) {
if ( empty( $post->post_parent ) ) {
$top_level[] = $post->ID;
} else {
$children[ $post->post_parent ][] = $post->ID;
}
}

$ids = array();
$levels = array();
self::add_hierarchical_ids( $ids, $levels, 0, $top_level, $children );

// Process remaining children.
if ( ! empty( $children ) ) {
foreach ( $children as $parent_id => $child_ids ) {
$level = 0;
$ancestor = $parent_id;
while ( 0 !== $ancestor ) {
++$level;
$ancestor = self::get_ancestor( $ancestor );
}
self::add_hierarchical_ids( $ids, $levels, $level, $child_ids, $children );
}
}

return array(
'post_ids' => $ids,
'levels' => $levels,
);
}

private static function add_hierarchical_ids( &$ids, &$levels, $level, $to_process, $children ) {
foreach ( $to_process as $id ) {
if ( in_array( $id, $ids, true ) ) {
continue;
}
$ids[] = $id;
$levels[ $id ] = $level;

if ( isset( $children[ $id ] ) ) {
self::add_hierarchical_ids( $ids, $levels, $level + 1, $children[ $id ], $children );
unset( $children[ $id ] );
}
}
}

public static function get_post_ids() {
return self::$post_ids;
}

public static function get_levels() {
return self::$levels;
}
}

add_filter(
'rest_page_collection_params',
function ( $params ) {
$params['orderby_hierarchy'] = array(
'description' => 'Sort pages by hierarchy.',
'type' => 'boolean',
'default' => false,
);
return $params;
}
);

add_filter(
'rest_page_query',
function ( $args, $request ) {
if ( ! Gutenberg_Hierarchical_Sort::is_eligible( $request ) ) {
return $args;
}

$hs = Gutenberg_Hierarchical_Sort::get_instance();
$hs->run( $args );

// Reconfigure the args to display only the ids in the list.
$args['post__in'] = $hs->get_post_ids();
$args['orderby'] = 'post__in';

return $args;
},
10,
2
);

add_filter(
'rest_prepare_page',
function ( $response, $post, $request ) {
if ( ! Gutenberg_Hierarchical_Sort::is_eligible( $request ) ) {
return $response;
}

$hs = Gutenberg_Hierarchical_Sort::get_instance();
$response->data['level'] = $hs->get_levels()[ $post->ID ];

return $response;
},
10,
3
);
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.8/block-comments.php';
require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-comment-controller-6-8.php';
require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php';
require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php';
require __DIR__ . '/compat/wordpress-6.8/rest-api.php';

// Plugin specific code.
Expand Down
8 changes: 6 additions & 2 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@

- Fixed commonjs export ([#67962](https://github.com/WordPress/gutenberg/pull/67962))

### Features

- Add support for hierarchical visualization of data. `DataViews` gets a new prop `getItemLevel` that should return the hierarchical level of the item. The view can use `view.showLevels` to display the levels. It's up to the consumer data source to prepare this information.

## 4.10.0 (2024-12-11)

## Breaking Changes
### Breaking Changes

- Support showing or hiding title, media and description fields ([#67477](https://github.com/WordPress/gutenberg/pull/67477)).
- Unify the `title`, `media` and `description` fields for the different layouts. So instead of the previous `view.layout.mediaField`, `view.layout.primaryField` and `view.layout.columnFields`, all the layouts now support these three fields with the following config ([#67477](https://github.com/WordPress/gutenberg/pull/67477)):
Expand All @@ -23,7 +27,7 @@ const view = {
};
```

## Internal
### Internal

- Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)).
- Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)).
Expand Down
14 changes: 14 additions & 0 deletions packages/dataviews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ Example:
}
```
#### `getItemLevel`: `function`

A function that receives an item and returns its hierarchical level. It's optional, but this property must be passed for DataViews to display the hierarchical levels of the data if `view.showLevels` is true.

Example:

```js
// Example implementation
{
getItemLevel={ ( item ) => item.level }
}
```
#### `fields`: `Object[]`
The fields describe the visible items for each record in the dataset and how they behave (how to sort them, display them, etc.). See "Fields API" for a description of every property.
Expand Down Expand Up @@ -193,6 +206,7 @@ Properties:
- `showTitle`: Whether the title should be shown in the UI. `true` by default.
- `showMedia`: Whether the media should be shown in the UI. `true` by default.
- `showDescription`: Whether the description should be shown in the UI. `true` by default.
- `showLevels`: Whether to display the hierarchical levels for the data. `false` by default. See related `getItemLevel` DataView prop.
- `fields`: a list of remaining field `id` that are visible in the UI and the specific order in which they are displayed.
- `layout`: config that is specific to a particular layout type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type DataViewsContextType< Item > = {
openedFilter: string | null;
setOpenedFilter: ( openedFilter: string | null ) => void;
getItemId: ( item: Item ) => string;
getItemLevel?: ( item: Item ) => number;
onClickItem?: ( item: Item ) => void;
isItemClickable: ( item: Item ) => boolean;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/dataviews/src/components/dataviews-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function DataViewsLayout() {
data,
fields,
getItemId,
getItemLevel,
isLoading,
view,
onChangeView,
Expand All @@ -40,6 +41,7 @@ export default function DataViewsLayout() {
data={ data }
fields={ fields }
getItemId={ getItemId }
getItemLevel={ getItemLevel }
isLoading={ isLoading }
onChangeView={ onChangeView }
onChangeSelection={ onChangeSelection }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ function SortFieldControl() {
direction: view?.sort?.direction || 'desc',
field: value,
},
showLevels: false,
} );
} }
/>
Expand Down Expand Up @@ -194,6 +195,7 @@ function SortDirectionControl() {
)?.id ||
'',
},
showLevels: false,
} );
return;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/dataviews/src/components/dataviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type DataViewsProps< Item > = {
onClickItem?: ( item: Item ) => void;
isItemClickable?: ( item: Item ) => boolean;
header?: ReactNode;
getItemLevel?: ( item: Item ) => number;
} & ( Item extends ItemWithId
? { getItemId?: ( item: Item ) => string }
: { getItemId: ( item: Item ) => string } );
Expand All @@ -64,6 +65,7 @@ export default function DataViews< Item >( {
actions = EMPTY_ARRAY,
data,
getItemId = defaultGetItemId,
getItemLevel,
isLoading = false,
paginationInfo,
defaultLayouts,
Expand Down Expand Up @@ -115,6 +117,7 @@ export default function DataViews< Item >( {
openedFilter,
setOpenedFilter,
getItemId,
getItemLevel,
isItemClickable,
onClickItem,
} }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ const _HeaderMenu = forwardRef( function HeaderMenu< Item >(
field: fieldId,
direction,
},
showLevels: false,
} );
} }
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import getClickableItemProps from '../utils/get-clickable-item-props';

function ColumnPrimary< Item >( {
item,
level,
titleField,
mediaField,
descriptionField,
onClickItem,
isItemClickable,
}: {
item: Item;
level?: number;
titleField?: NormalizedField< Item >;
mediaField?: NormalizedField< Item >;
descriptionField?: NormalizedField< Item >;
Expand All @@ -44,6 +46,11 @@ function ColumnPrimary< Item >( {
<VStack spacing={ 0 }>
{ titleField && (
<div { ...clickableProps }>
{ level !== undefined && (
<span className="dataviews-view-table__level">
{ '—'.repeat( level ) }&nbsp;
</span>
) }
<titleField.render item={ item } />
</div>
) }
Expand Down
Loading

1 comment on commit 2e3e6e4

@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 2e3e6e4.
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/12395971821
📝 Reported issues:

Please sign in to comment.