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

Update bulk header with actions #67743

Merged
merged 4 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
88 changes: 0 additions & 88 deletions packages/edit-site/src/components/post-edit/header.js

This file was deleted.

5 changes: 2 additions & 3 deletions packages/edit-site/src/components/post-edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ import { privateApis as editorPrivateApis } from '@wordpress/editor';
* Internal dependencies
*/
import Page from '../page';
import PostEditHeader from '../post-edit/header';
import { unlock } from '../../lock-unlock';
import usePatternSettings from '../page-patterns/use-pattern-settings';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';

const { usePostFields } = unlock( editorPrivateApis );
const { usePostFields, PostCardPanel } = unlock( editorPrivateApis );

const fieldsWithBulkEditSupport = [
'title',
Expand Down Expand Up @@ -159,7 +158,7 @@ function PostEditForm( { postType, postId } ) {

return (
<VStack spacing={ 4 }>
<PostEditHeader postType={ postType } postId={ postId } />
<PostCardPanel postType={ postType } postId={ ids } />
<DataForm
data={ ids.length === 1 ? record : multiEdits }
fields={ fieldsWithDependency }
Expand Down
10 changes: 0 additions & 10 deletions packages/edit-site/src/components/post-edit/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,3 @@
padding-top: $grid-unit-20;
}
}

.edit-site-post-edit-header {
.edit-site-post-edit-header__description {
color: $gray-700;
}

.edit-site-post-edit-header__title {
@include heading-medium();
}
}
61 changes: 40 additions & 21 deletions packages/editor/src/components/post-actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,57 @@ import { usePostActions } from './actions';

const { Menu, kebabCase } = unlock( componentsPrivateApis );

export default function PostActions( { postType, postId, onActionPerformed } ) {
const [ activeModalAction, setActiveModalAction ] = useState( null );
const { item, permissions } = useSelect(
function useEditedEntityRecordsWithPermissions( postType, postIds ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved this logic to its own hook. It gets all the edited records and adds permissions.
It's similar to this hook:

export function useEntityRecordsWithPermissions< RecordType >(
but than for edited records specifically.

Would it be worth moving to the core-data package as well?

const { items, permissions } = useSelect(
( select ) => {
const { getEditedEntityRecord, getEntityRecordPermissions } =
unlock( select( coreStore ) );
return {
item: getEditedEntityRecord( 'postType', postType, postId ),
permissions: getEntityRecordPermissions(
'postType',
postType,
postId
items: postIds.map( ( postId ) =>
getEditedEntityRecord( 'postType', postType, postId )
),
permissions: postIds.map( ( postId ) =>
getEntityRecordPermissions( 'postType', postType, postId )
Comment on lines +29 to +33
Copy link
Member

Choose a reason for hiding this comment

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

This change has a couple of issues:

  • Iterating over values here results in different items and permissions arrays on every mapSelect call.
  • Technically, it can also result in multiple HTTP calls if entity data isn't available in the store.

You can fetch permissions for multiple entities using getEntityRecordsPermissions, but we don't have a similar substitute for getEditedEntityRecord. The getEntityRecords could work but will trigger a REST API request for each new post selection in DataViews.

Screenshot

CleanShot 2024-12-10 at 20 06 06

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @Mamaduka, will push up a fix for this today.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Followed up in this PR: #67872

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, @louwie17; I'll have a look later today or tomorrow.

),
};
},
[ postId, postType ]
[ postIds, postType ]
);
const itemWithPermissions = useMemo( () => {
return {

return useMemo( () => {
return items.map( ( item, index ) => ( {
...item,
permissions,
};
}, [ item, permissions ] );
permissions: permissions[ index ],
} ) );
}, [ items, permissions ] );
}

export default function PostActions( { postType, postId, onActionPerformed } ) {
const [ activeModalAction, setActiveModalAction ] = useState( null );
const _postIds = useMemo( () => {
if ( Array.isArray( postId ) ) {
return postId;
}
return postId ? [ postId ] : [];
}, [ postId ] );

const itemsWithPermissions = useEditedEntityRecordsWithPermissions(
postType,
_postIds
);
const allActions = usePostActions( { postType, onActionPerformed } );

const actions = useMemo( () => {
return allActions.filter( ( action ) => {
return (
! action.isEligible || action.isEligible( itemWithPermissions )
( ! action.isEligible ||
itemsWithPermissions.some( ( itemWithPermissions ) =>
action.isEligible( itemWithPermissions )
) ) &&
( itemsWithPermissions.length < 2 || action.supportsBulk )
Copy link
Contributor

Choose a reason for hiding this comment

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

Remark: Some of this logic and the UI component itself to render the actions dropdown... is already present in the DataViews package. It seems like instead of duplicating all of that, we should be exposing a DataActions component from the DataViews package :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that would make sense, as there is a lot of duplication. But it appears this was also done on purpose, given this inline comment:
https://github.com/WordPress/gutenberg/blob/trunk/packages/editor/src/components/post-actions/index.js#L88-L91

From now on all the functions on this file are copied as from the dataviews packages,
The editor packages should not be using the dataviews packages directly,
and the dataviews package should not be using the editor packages directly,
so duplicating the code here seems like the least bad option.

Should we still go ahead with this, or is there a specific reason these packages can't depend on each other?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why the editor shouldn't be using dataviews package.

cc @jorgefilipecosta

(Regardless, this should be a follow-up and not block this PR from shipping)

);
} );
}, [ allActions, itemWithPermissions ] );
}, [ allActions, itemsWithPermissions ] );

return (
<>
Expand All @@ -70,14 +89,14 @@ export default function PostActions( { postType, postId, onActionPerformed } ) {
>
<ActionsDropdownMenuGroup
actions={ actions }
item={ itemWithPermissions }
items={ itemsWithPermissions }
setActiveModalAction={ setActiveModalAction }
/>
</Menu>
{ !! activeModalAction && (
<ActionModal
action={ activeModalAction }
items={ [ item ] }
items={ itemsWithPermissions }
closeModal={ () => setActiveModalAction( null ) }
/>
) }
Expand Down Expand Up @@ -119,7 +138,7 @@ export function ActionModal( { action, items, closeModal } ) {
);
}

function ActionsDropdownMenuGroup( { actions, item, setActiveModalAction } ) {
function ActionsDropdownMenuGroup( { actions, items, setActiveModalAction } ) {
const registry = useRegistry();
return (
<Menu.Group>
Expand All @@ -133,9 +152,9 @@ function ActionsDropdownMenuGroup( { actions, item, setActiveModalAction } ) {
setActiveModalAction( action );
return;
}
action.callback( [ item ], { registry } );
action.callback( items, { registry } );
} }
items={ [ item ] }
items={ items }
/>
);
} ) }
Expand Down
90 changes: 64 additions & 26 deletions packages/editor/src/components/post-card-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import {
Icon,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
__experimentalText as Text,
} from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';

/**
Expand All @@ -24,50 +26,77 @@ import PostActions from '../post-actions';
import usePageTypeBadge from '../../utils/pageTypeBadge';
import { getTemplateInfo } from '../../utils/get-template-info';

/**
* Renders a title of the post type and the available quick actions available within a 3-dot dropdown.
*
* @param {Object} props - Component props.
* @param {string} [props.postType] - The post type string.
* @param {string|string[]} [props.postId] - The post id or list of post ids.
* @param {Function} [props.onActionPerformed] - A callback function for when a quick action is performed.
* @return {React.ReactNode} The rendered component.
*/
export default function PostCardPanel( {
postType,
postId,
onActionPerformed,
} ) {
const { title, icon } = useSelect(
const postIds = useMemo(
() => ( Array.isArray( postId ) ? postId : [ postId ] ),
[ postId ]
);
const { postTitle, icon, labels } = useSelect(
( select ) => {
const { getEditedEntityRecord } = select( coreStore );
const { getEditedEntityRecord, getEntityRecord, getPostType } =
select( coreStore );
const { getPostIcon } = unlock( select( editorStore ) );
let _title = '';
const _record = getEditedEntityRecord(
'postType',
postType,
postId
postIds[ 0 ]
);
if ( postIds.length === 1 ) {
const { default_template_types: templateTypes = [] } =
getEntityRecord( 'root', '__unstableBase' ) ?? {};

const { default_template_types: templateTypes = [] } =
select( coreStore ).getEntityRecord(
'root',
'__unstableBase'
) ?? {};

const _templateInfo = [
TEMPLATE_POST_TYPE,
TEMPLATE_PART_POST_TYPE,
].includes( postType )
? getTemplateInfo( {
template: _record,
templateTypes,
} )
: {};
const _templateInfo = [
TEMPLATE_POST_TYPE,
TEMPLATE_PART_POST_TYPE,
].includes( postType )
? getTemplateInfo( {
template: _record,
templateTypes,
} )
: {};
_title = _templateInfo?.title || _record?.title;
}

return {
title: _templateInfo?.title || _record?.title,
icon: unlock( select( editorStore ) ).getPostIcon( postType, {
postTitle: _title,
icon: getPostIcon( postType, {
area: _record?.area,
} ),
labels: getPostType( postType )?.labels,
};
},
[ postId, postType ]
[ postIds, postType ]
);

const pageTypeBadge = usePageTypeBadge( postId );
let title = __( 'No title' );
if ( labels?.name && postIds.length > 1 ) {
title = sprintf(
// translators: %i number of selected items %s: Name of the plural post type e.g: "Posts".
__( '%i %s' ),
postId.length,
labels?.name
);
} else if ( postTitle ) {
title = decodeEntities( postTitle );
}

return (
<div className="editor-post-card-panel">
<VStack spacing={ 1 } className="editor-post-card-panel">
<HStack
spacing={ 2 }
className="editor-post-card-panel__header"
Expand All @@ -80,8 +109,8 @@ export default function PostCardPanel( {
className="editor-post-card-panel__title"
as="h2"
>
{ title ? decodeEntities( title ) : __( 'No title' ) }
{ pageTypeBadge && (
{ title }
{ pageTypeBadge && postIds.length === 1 && (
<span className="editor-post-card-panel__title-badge">
{ pageTypeBadge }
</span>
Expand All @@ -93,6 +122,15 @@ export default function PostCardPanel( {
onActionPerformed={ onActionPerformed }
/>
</HStack>
</div>
{ postIds.length > 1 && (
<Text className="editor-post-card-panel__description">
{ sprintf(
// translators: %s: Name of the plural post type e.g: "Posts".
__( 'Changes will be applied to all selected %s.' ),
labels?.name.toLowerCase()
) }
</Text>
) }
</VStack>
);
}
4 changes: 4 additions & 0 deletions packages/editor/src/components/post-card-panel/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
&.has-description &__header {
margin-bottom: $grid-unit-10;
}

.editor-post-card-panel__description {
color: $gray-700;
}
}

.editor-post-card-panel__title-badge {
Expand Down
Loading
Loading