Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataViews: label prop in Actions API can be either a string or a function #61942

Merged
merged 4 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

- Remove some unused dependencies ([#62010](https://github.com/WordPress/gutenberg/pull/62010)).

### Enhancement

- `label` prop in Actions API can be either a `sting` value or a `function`, in case we want to use information from the selected items. ([#61942](https://github.com/WordPress/gutenberg/pull/61942)).

## 1.2.0 (2024-05-16)

### Internal
Expand Down
63 changes: 33 additions & 30 deletions packages/dataviews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ npm install @wordpress/dataviews --save

```jsx
const Example = () => {

// Declare data, fields, etc.

return (
Expand All @@ -27,7 +26,7 @@ const Example = () => {
paginationInfo={ paginationInfo }
/>
);
}
};
```

## Properties
Expand All @@ -42,12 +41,14 @@ Example:
const data = [
{
id: 1,
title: "Title",
author: "Admin",
date: "2012-04-23T18:25:43.511Z"
title: 'Title',
author: 'Admin',
date: '2012-04-23T18:25:43.511Z',
},
{ /* ... */ }
]
{
/* ... */
},
];
```

By default, dataviews would use each record's `id` as an unique identifier. If it's not, the consumer should provide a `getItemId` function that returns one.
Expand Down Expand Up @@ -125,8 +126,8 @@ Each field is an object with the following properties:
- `enableSorting`: whether the data can be sorted by the given field. True by default.
- `enableHiding`: whether the field can be hidden. True by default.
- `filterBy`: configuration for the filters.
- `operators`: the list of operators supported by the field.
- `isPrimary`: whether it is a primary filter. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter.
- `operators`: the list of operators supported by the field.
- `isPrimary`: whether it is a primary filter. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter.

### `view`: `object`

Expand All @@ -140,7 +141,7 @@ const view = {
search: '',
filters: [
{ field: 'author', operator: 'is', value: 2 },
{ field: 'status', operator: 'isAny', value: [ 'publish', 'draft'] }
{ field: 'status', operator: 'isAny', value: [ 'publish', 'draft' ] },
],
page: 1,
perPage: 5,
Expand All @@ -150,7 +151,7 @@ const view = {
},
hiddenFields: [ 'date', 'featured-image' ],
layout: {},
}
};
```

Properties:
Expand All @@ -164,8 +165,8 @@ Properties:
- `perPage`: number of records to show per page.
- `page`: the page that is visible.
- `sort`:
- `field`: the field used for sorting the dataset.
- `direction`: the direction to use for sorting, one of `asc` or `desc`.
- `field`: the field used for sorting the dataset.
- `direction`: the direction to use for sorting, one of `asc` or `desc`.
- `hiddenFields`: the `id` of the fields that are hidden in the UI.
- `layout`: config that is specific to a particular layout type.
- `mediaField`: used by the `grid` and `list` layouts. The `id` of the field to be used for rendering each card's media.
Expand All @@ -192,7 +193,11 @@ function MyCustomPageTable() {
search: '',
filters: [
{ field: 'author', operator: 'is', value: 2 },
{ field: 'status', operator: 'isAny', value: [ 'publish', 'draft' ] }
{
field: 'status',
operator: 'isAny',
value: [ 'publish', 'draft' ],
},
],
hiddenFields: [ 'date', 'featured-image' ],
layout: {},
Expand All @@ -219,9 +224,7 @@ function MyCustomPageTable() {
};
}, [ view ] );

const {
records
} = useEntityRecords( 'postType', 'page', queryArgs );
const { records } = useEntityRecords( 'postType', 'page', queryArgs );

return (
<DataViews
Expand All @@ -241,7 +244,7 @@ Collection of operations that can be performed upon each record.
Each action is an object with the following properties:

- `id`: string, required. Unique identifier of the action. For example, `move-to-trash`.
- `label`: string, required. User facing description of the action. For example, `Move to Trash`.
- `label`: string|function, required. User facing description of the action. For example, `Move to Trash`. In case we want to adjust the label based on the selected items, a getter function which accepts the selected records as input can be provided. The getter function should always return a `string` value.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Besides this change, everything else was from auto formatting.

- `isPrimary`: boolean, optional. Whether the action should be listed inline (primary) or in hidden in the more actions menu (secondary).
- `icon`: icon to show for primary actions. It's required for a primary action, otherwise the action would be considered secondary.
- `isEligible`: function, optional. Whether the action can be performed for a given record. If not present, the action is considered to be eligible for all items. It takes the given record as input.
Expand All @@ -252,8 +255,8 @@ Each action is an object with the following properties:

### `paginationInfo`: `Object`

- `totalItems`: the total number of items in the datasets.
- `totalPages`: the total number of pages, taking into account the total items in the dataset and the number of items per page provided by the user.
- `totalItems`: the total number of items in the datasets.
- `totalPages`: the total number of pages, taking into account the total items in the dataset and the number of items per page provided by the user.

### `search`: `boolean`

Expand Down Expand Up @@ -283,9 +286,9 @@ Callback that signals the user selected one of more items, and takes them as par

### Layouts

- `table`: the view uses a table layout.
- `grid`: the view uses a grid layout.
- `list`: the view uses a list layout.
- `table`: the view uses a table layout.
- `grid`: the view uses a grid layout.
- `list`: the view uses a list layout.

### Fields

Expand All @@ -295,13 +298,13 @@ Callback that signals the user selected one of more items, and takes them as par

Allowed operators:

| Operator | Selection | Description | Example |
| --- | --- | --- | --- |
| `is` | Single item | `EQUAL TO`. The item's field is equal to a single value. | Author is Admin |
| `isNot` | Single item | `NOT EQUAL TO`. The item's field is not equal to a single value. | Author is not Admin |
| `isAny` | Multiple items | `OR`. The item's field is present in a list of values. | Author is any: Admin, Editor |
| `isNone` | Multiple items | `NOT OR`. The item's field is not present in a list of values. | Author is none: Admin, Editor |
| `isAll` | Multiple items | `AND`. The item's field has all of the values in the list. | Category is all: Book, Review, Science Fiction |
| Operator | Selection | Description | Example |
| ---------- | -------------- | ----------------------------------------------------------------------- | -------------------------------------------------- |
| `is` | Single item | `EQUAL TO`. The item's field is equal to a single value. | Author is Admin |
| `isNot` | Single item | `NOT EQUAL TO`. The item's field is not equal to a single value. | Author is not Admin |
| `isAny` | Multiple items | `OR`. The item's field is present in a list of values. | Author is any: Admin, Editor |
| `isNone` | Multiple items | `NOT OR`. The item's field is not present in a list of values. | Author is none: Admin, Editor |
| `isAll` | Multiple items | `AND`. The item's field has all of the values in the list. | Category is all: Book, Review, Science Fiction |
| `isNotAll` | Multiple items | `NOT AND`. The item's field doesn't have all of the values in the list. | Category is not all: Book, Review, Science Fiction |

`is` and `isNot` are single-selection operators, while `isAny`, `isNone`, `isAll`, and `isNotALl` are multi-selection. By default, a filter with no operators declared will support the `isAny` and `isNone` multi-selection operators. A filter cannot mix single-selection & multi-selection operators; if a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded and the filter won't allow selecting more than one item.
Expand Down
6 changes: 5 additions & 1 deletion packages/dataviews/src/bulk-actions-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ function ActionTrigger< Item extends AnyItem >( {
action,
onClick,
isBusy,
items,
}: ActionTriggerProps< Item > ) {
const label =
typeof action.label === 'string' ? action.label : action.label( items );
return (
<ToolbarButton
disabled={ isBusy }
label={ action.label }
label={ label }
icon={ action.icon }
isDestructive={ action.isDestructive }
size="compact"
Expand Down Expand Up @@ -112,6 +115,7 @@ function ActionButton< Item extends AnyItem >( {
setActionInProgress( action.id );
action.callback( selectedItems );
} }
items={ selectedEligibleItems }
isBusy={ actionInProgress === action.id }
/>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/dataviews/src/bulk-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,13 @@ function ActionWithModal< Item extends AnyItem >( {
const onCloseModal = useCallback( () => {
setActionWithModal( undefined );
}, [ setActionWithModal ] );
const label =
typeof action.label === 'string'
? action.label
: action.label( selectedItems );
return (
<Modal
title={ ! hideModalHeader ? action.label : undefined }
title={ ! hideModalHeader ? label : undefined }
__experimentalHideHeader={ !! hideModalHeader }
onRequestClose={ onCloseModal }
overlayClassName="dataviews-action-modal"
Expand Down
17 changes: 14 additions & 3 deletions packages/dataviews/src/item-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface ActionTriggerProps< Item extends AnyItem > {
action: Action< Item >;
onClick: MouseEventHandler;
isBusy?: boolean;
items: Item[];
}

interface ActionModalProps< Item extends AnyItem > {
Expand Down Expand Up @@ -67,10 +68,13 @@ interface CompactItemActionsProps< Item extends AnyItem > {
function ButtonTrigger< Item extends AnyItem >( {
action,
onClick,
items,
}: ActionTriggerProps< Item > ) {
const label =
typeof action.label === 'string' ? action.label : action.label( items );
return (
<Button
label={ action.label }
label={ label }
icon={ action.icon }
isDestructive={ action.isDestructive }
size="compact"
Expand All @@ -82,13 +86,16 @@ function ButtonTrigger< Item extends AnyItem >( {
function DropdownMenuItemTrigger< Item extends AnyItem >( {
action,
onClick,
items,
}: ActionTriggerProps< Item > ) {
const label =
typeof action.label === 'string' ? action.label : action.label( items );
return (
<DropdownMenuItem
onClick={ onClick }
hideOnClick={ ! ( 'RenderModal' in action ) }
>
<DropdownMenuItemLabel>{ action.label }</DropdownMenuItemLabel>
<DropdownMenuItemLabel>{ label }</DropdownMenuItemLabel>
</DropdownMenuItem>
);
}
Expand All @@ -98,9 +105,11 @@ export function ActionModal< Item extends AnyItem >( {
items,
closeModal,
}: ActionModalProps< Item > ) {
const label =
typeof action.label === 'string' ? action.label : action.label( items );
return (
<Modal
title={ action.modalHeader || action.label }
title={ action.modalHeader || label }
__experimentalHideHeader={ !! action.hideModalHeader }
onRequestClose={ closeModal ?? ( () => {} ) }
overlayClassName={ `dataviews-action-modal dataviews-action-modal__${ kebabCase(
Expand Down Expand Up @@ -168,6 +177,7 @@ export function ActionsDropdownMenuGroup< Item extends AnyItem >( {
key={ action.id }
action={ action }
onClick={ () => action.callback( [ item ] ) }
items={ [ item ] }
/>
);
} ) }
Expand Down Expand Up @@ -224,6 +234,7 @@ export default function ItemActions< Item extends AnyItem >( {
key={ action.id }
action={ action }
onClick={ () => action.callback( [ item ] ) }
items={ item[ 0 ] }
/>
);
} ) }
Expand Down
4 changes: 3 additions & 1 deletion packages/dataviews/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,10 @@ interface ActionBase< Item extends AnyItem > {

/**
* The label of the action.
* In case we want to adjust the label based on the selected items,
* a function can be provided.
*/
label: string;
label: string | ( ( items: Item[] ) => string );

/**
* The icon of the action. (Either a string or an SVG element)
Expand Down
9 changes: 7 additions & 2 deletions packages/dataviews/src/view-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ function ListItem< Item extends AnyItem >( {
}, [ actions, item ] );

const [ isModalOpen, setIsModalOpen ] = useState( false );
const primaryActionLabel =
primaryAction &&
( typeof primaryAction.label === 'string'
? primaryAction.label
: primaryAction.label( [ item ] ) );

return (
<CompositeRow
Expand Down Expand Up @@ -193,7 +198,7 @@ function ListItem< Item extends AnyItem >( {
store={ store }
render={
<Button
label={ primaryAction.label }
label={ primaryActionLabel }
icon={ primaryAction.icon }
isDestructive={
primaryAction.isDestructive
Expand Down Expand Up @@ -224,7 +229,7 @@ function ListItem< Item extends AnyItem >( {
store={ store }
render={
<Button
label={ primaryAction.label }
label={ primaryActionLabel }
icon={ primaryAction.icon }
isDestructive={
primaryAction.isDestructive
Expand Down
10 changes: 9 additions & 1 deletion packages/editor/src/components/post-actions/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,15 @@ const viewPostAction = {

const postRevisionsAction = {
id: 'view-post-revisions',
label: __( 'View revisions' ),
label( items ) {
const revisionsCount =
items[ 0 ]._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0;
return sprintf(
/* translators: %s: number of revisions */
__( 'View revisions (%s)' ),
Copy link
Member

Choose a reason for hiding this comment

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

This approach works but I'm not sure from the design point of view if the idea was to use a suffix in the button, something like:
Screenshot 2024-05-27 at 12 38 20

It would make things complex in terms of API, as we would need something besides the label.

revisionsCount
);
},
isPrimary: false,
isEligible: ( post ) => {
if ( post.status === 'trash' ) {
Expand Down
8 changes: 6 additions & 2 deletions packages/editor/src/components/post-actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ export default function PostActions( { onActionPerformed, buttonProps } ) {
// so duplicating the code here seems like the least bad option.

// Copied as is from packages/dataviews/src/item-actions.js
function DropdownMenuItemTrigger( { action, onClick } ) {
function DropdownMenuItemTrigger( { action, onClick, items } ) {
const label =
typeof action.label === 'string' ? action.label : action.label( items );
return (
<DropdownMenuItem
onClick={ onClick }
hideOnClick={ ! action.RenderModal }
>
<DropdownMenuItemLabel>{ action.label }</DropdownMenuItemLabel>
<DropdownMenuItemLabel>{ label }</DropdownMenuItemLabel>
</DropdownMenuItem>
);
}
Expand All @@ -105,6 +107,7 @@ function ActionWithModal( { action, item, ActionTrigger, onClose } ) {
const actionTriggerProps = {
action,
onClick: () => setIsModalOpen( true ),
items: [ item ],
};
const { RenderModal, hideModalHeader } = action;
return (
Expand Down Expand Up @@ -156,6 +159,7 @@ function ActionsDropdownMenuGroup( { actions, item, onClose } ) {
key={ action.id }
action={ action }
onClick={ () => action.callback( [ item ] ) }
items={ [ item ] }
/>
);
} ) }
Expand Down
Loading