refactor(ui): moves bulk edit controls (#11332)

Bulk edit controls are currently displayed within the search bar of the
list view. This doesn't make sense from a UX perspective, as the current
selection is displayed somewhere else entirely. These controls also take
up a lot of visual real estate which is beginning to get overused
especially after the introduction of "list menu items" in #11230, and
the potential introduction of "saved filters" controls in #11330.

Now, they are rendered contextually _alongside_ the selection count. To
make room for these new controls, they are displayed in plain text and
the entity labels have been removed from the selection count.
This commit is contained in:
Jacob Fletcher
2025-02-21 15:06:30 -05:00
committed by GitHub
parent a8bec9a1b2
commit f31568c69c
13 changed files with 150 additions and 81 deletions

View File

@@ -7,5 +7,21 @@
align-items: center;
justify-content: center;
height: 100%;
&__toggle {
font-size: inherit;
line-height: inherit;
display: inline-flex;
background: transparent;
color: var(--theme-elevation-800);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 0;
padding: 0;
align-items: center;
cursor: pointer;
text-decoration: underline;
}
}
}

View File

@@ -16,7 +16,6 @@ import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js'
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js'
import { Pill } from '../Pill/index.js'
import './index.scss'
const baseClass = 'delete-documents'
@@ -140,14 +139,15 @@ export const DeleteMany: React.FC<Props> = (props) => {
return (
<React.Fragment>
<Pill
<button
className={`${baseClass}__toggle`}
onClick={() => {
openModal(modalSlug)
}}
type="button"
>
{t('general:delete')}
</Pill>
</button>
<ConfirmationModal
body={t('general:aboutToDeleteCount', {
count,

View File

@@ -3,33 +3,19 @@
@layer payload-default {
.edit-many {
&__toggle {
font-size: 1rem;
line-height: base(1.2);
font-size: inherit;
line-height: inherit;
display: inline-flex;
background: var(--theme-elevation-150);
background: transparent;
color: var(--theme-elevation-800);
border-radius: $style-radius-s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 0;
padding: 0 base(0.4);
padding: 0;
align-items: center;
cursor: pointer;
text-decoration: none;
&:active,
&:focus {
outline: none;
}
&:hover {
background: var(--theme-elevation-100);
}
&:active {
background: var(--theme-elevation-100);
}
text-decoration: underline;
}
&__form {

View File

@@ -1,15 +1,16 @@
'use client'
import type { ClientCollectionConfig, FieldWithPathClient } from 'payload'
import { useModal } from '@faceless-ui/modal'
import React, { useState } from 'react'
import { useAuth } from '../../providers/Auth/index.js'
import { EditDepthProvider } from '../../providers/EditDepth/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import './index.scss'
import { Drawer, DrawerToggler } from '../Drawer/index.js'
import { Drawer } from '../Drawer/index.js'
import { EditManyDrawerContent } from './DrawerContent.js'
import './index.scss'
export const baseClass = 'edit-many'
@@ -23,6 +24,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
} = props
const { permissions } = useAuth()
const { openModal } = useModal()
const { selectAll } = useSelection()
const { t } = useTranslation()
@@ -39,16 +41,17 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
return (
<div className={baseClass}>
<DrawerToggler
<button
aria-label={t('general:edit')}
className={`${baseClass}__toggle`}
onClick={() => {
openModal(drawerSlug)
setSelected([])
}}
slug={drawerSlug}
type="button"
>
{t('general:edit')}
</DrawerToggler>
</button>
<EditDepthProvider>
<Drawer Header={null} slug={drawerSlug}>
<EditManyDrawerContent

View File

@@ -3,7 +3,7 @@ import type { ClientCollectionConfig, ResolvedFilterOptions, Where } from 'paylo
import { useWindowInfo } from '@faceless-ui/window-info'
import { getTranslation } from '@payloadcms/translations'
import React, { Fragment, useEffect, useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { Popup, PopupList } from '../../elements/Popup/index.js'
import { useUseTitleField } from '../../hooks/useUseAsTitle.js'
@@ -14,12 +14,8 @@ import { useListQuery } from '../../providers/ListQuery/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { AnimateHeight } from '../AnimateHeight/index.js'
import { ColumnSelector } from '../ColumnSelector/index.js'
import { DeleteMany } from '../DeleteMany/index.js'
import { EditMany } from '../EditMany/index.js'
import { Pill } from '../Pill/index.js'
import { PublishMany } from '../PublishMany/index.js'
import { SearchFilter } from '../SearchFilter/index.js'
import { UnpublishMany } from '../UnpublishMany/index.js'
import { WhereBuilder } from '../WhereBuilder/index.js'
import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js'
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
@@ -31,7 +27,15 @@ export type ListControlsProps = {
readonly beforeActions?: React.ReactNode[]
readonly collectionConfig: ClientCollectionConfig
readonly collectionSlug: string
/**
* @deprecated
* These are now handled by the `ListSelection` component
*/
readonly disableBulkDelete?: boolean
/**
* @deprecated
* These are now handled by the `ListSelection` component
*/
readonly disableBulkEdit?: boolean
readonly enableColumns?: boolean
readonly enableSort?: boolean
@@ -53,8 +57,6 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
beforeActions,
collectionConfig,
collectionSlug,
disableBulkDelete,
disableBulkEdit,
enableColumns = true,
enableSort = false,
listMenuItems,
@@ -148,19 +150,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
/>
<div className={`${baseClass}__buttons`}>
<div className={`${baseClass}__buttons-wrap`}>
{!smallBreak && (
<React.Fragment>
{beforeActions && beforeActions}
{!disableBulkEdit && (
<Fragment>
<EditMany collection={collectionConfig} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
</Fragment>
)}
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
</React.Fragment>
)}
{!smallBreak && <React.Fragment>{beforeActions && beforeActions}</React.Fragment>}
{enableColumns && (
<Pill
aria-controls={`${baseClass}-columns`}

View File

@@ -1,17 +1,31 @@
'use client'
import type { ClientCollectionConfig } from 'payload'
import React, { Fragment } from 'react'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { DeleteMany } from '../DeleteMany/index.js'
import { EditMany } from '../EditMany/index.js'
import { PublishMany } from '../PublishMany/index.js'
import { UnpublishMany } from '../UnpublishMany/index.js'
import './index.scss'
const baseClass = 'list-selection'
export type ListSelectionProps = {
collectionConfig?: ClientCollectionConfig
disableBulkDelete?: boolean
disableBulkEdit?: boolean
label: string
}
export const ListSelection: React.FC<ListSelectionProps> = ({ label }) => {
export const ListSelection: React.FC<ListSelectionProps> = ({
collectionConfig,
disableBulkDelete,
disableBulkEdit,
label,
}) => {
const { count, selectAll, toggleAll, totalDocs } = useSelection()
const { t } = useTranslation()
@@ -21,10 +35,8 @@ export const ListSelection: React.FC<ListSelectionProps> = ({ label }) => {
return (
<div className={baseClass}>
<span>{t('general:selectedCount', { count, label })}</span>
<span>{t('general:selectedCount', { count, label: '' })}</span>
{selectAll !== SelectAllStatus.AllAvailable && count < totalDocs && (
<Fragment>
<span>&mdash;</span>
<button
aria-label={t('general:selectAll', { count, label })}
className={`${baseClass}__button`}
@@ -32,10 +44,18 @@ export const ListSelection: React.FC<ListSelectionProps> = ({ label }) => {
onClick={() => toggleAll(true)}
type="button"
>
{t('general:selectAll', { count: totalDocs, label })}
{t('general:selectAll', { count: totalDocs, label: '' })}
</button>
)}
{!disableBulkEdit && !disableBulkDelete && <span>&mdash;</span>}
{!disableBulkEdit && (
<Fragment>
<EditMany collection={collectionConfig} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
</Fragment>
)}
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
</div>
)
}

View File

@@ -0,0 +1,21 @@
@import '../../scss/styles.scss';
@layer payload-default {
.publish-many {
&__toggle {
font-size: inherit;
line-height: inherit;
display: inline-flex;
background: transparent;
color: var(--theme-elevation-800);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 0;
padding: 0;
align-items: center;
cursor: pointer;
text-decoration: underline;
}
}
}

View File

@@ -16,7 +16,7 @@ import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js'
import { Pill } from '../Pill/index.js'
import './index.scss'
export type PublishManyProps = {
collection: ClientCollectionConfig
@@ -133,14 +133,15 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
return (
<React.Fragment>
<Pill
<button
className={`${baseClass}__toggle`}
onClick={() => {
openModal(modalSlug)
}}
type="button"
>
{t('version:publish')}
</Pill>
</button>
<ConfirmationModal
body={t('version:aboutToPublishSelection', { label: getTranslation(plural, i18n) })}
cancelLabel={t('general:cancel')}

View File

@@ -0,0 +1,21 @@
@import '../../scss/styles.scss';
@layer payload-default {
.unpublish-many {
&__toggle {
font-size: inherit;
line-height: inherit;
display: inline-flex;
background: transparent;
color: var(--theme-elevation-800);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 0;
padding: 0;
align-items: center;
cursor: pointer;
text-decoration: underline;
}
}
}

View File

@@ -16,7 +16,7 @@ import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js'
import { Pill } from '../Pill/index.js'
import './index.scss'
export type UnpublishManyProps = {
collection: ClientCollectionConfig
@@ -130,14 +130,15 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
return (
<React.Fragment>
<Pill
<button
className={`${baseClass}__toggle`}
onClick={() => {
toggleModal(modalSlug)
}}
type="button"
>
{t('version:unpublish')}
</Pill>
</button>
<ConfirmationModal
body={t('version:aboutToUnpublishSelection', { label: getTranslation(plural, i18n) })}
confirmingLabel={t('version:unpublishing')}

View File

@@ -22,6 +22,8 @@ export type ListHeaderProps = {
className?: string
collectionConfig: ClientCollectionConfig
Description?: React.ReactNode
disableBulkDelete?: boolean
disableBulkEdit?: boolean
hasCreatePermission: boolean
i18n: I18nClient
isBulkUploadEnabled: boolean
@@ -35,6 +37,8 @@ const DefaultListHeader: React.FC<ListHeaderProps> = ({
className,
collectionConfig,
Description,
disableBulkDelete,
disableBulkEdit,
hasCreatePermission,
i18n,
isBulkUploadEnabled,
@@ -72,7 +76,12 @@ const DefaultListHeader: React.FC<ListHeaderProps> = ({
</>
)}
{!smallBreak && (
<ListSelection label={getTranslation(collectionConfig?.labels?.plural, i18n)} />
<ListSelection
collectionConfig={collectionConfig}
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
label={getTranslation(collectionConfig?.labels?.plural, i18n)}
/>
)}
{Description}
</header>

View File

@@ -9,8 +9,6 @@ import React, { Fragment, useEffect, useState } from 'react'
import { useBulkUpload } from '../../elements/BulkUpload/index.js'
import { Button } from '../../elements/Button/index.js'
import { DeleteMany } from '../../elements/DeleteMany/index.js'
import { EditMany } from '../../elements/EditMany/index.js'
import { Gutter } from '../../elements/Gutter/index.js'
import { ListControls } from '../../elements/ListControls/index.js'
import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js'
@@ -18,13 +16,11 @@ import { ListSelection } from '../../elements/ListSelection/index.js'
import { useModal } from '../../elements/Modal/index.js'
import { Pagination } from '../../elements/Pagination/index.js'
import { PerPage } from '../../elements/PerPage/index.js'
import { PublishMany } from '../../elements/PublishMany/index.js'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { SelectMany } from '../../elements/SelectMany/index.js'
import { useStepNav } from '../../elements/StepNav/index.js'
import { RelationshipProvider } from '../../elements/Table/RelationshipProvider/index.js'
import { TableColumnsProvider } from '../../elements/TableColumns/index.js'
import { UnpublishMany } from '../../elements/UnpublishMany/index.js'
import { ViewDescription } from '../../elements/ViewDescription/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
@@ -185,6 +181,8 @@ export function DefaultListView(props: ListViewClientProps) {
/>
</div>
}
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
hasCreatePermission={hasCreatePermission}
i18n={i18n}
isBulkUploadEnabled={isBulkUploadEnabled && !upload.hideFileInputOnCreate}
@@ -203,8 +201,6 @@ export function DefaultListView(props: ListViewClientProps) {
}
collectionConfig={collectionConfig}
collectionSlug={collectionSlug}
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
listMenuItems={listMenuItems}
renderedFilters={renderedFilters}
resolvedFilterOptions={resolvedFilterOptions}
@@ -267,6 +263,9 @@ export function DefaultListView(props: ListViewClientProps) {
{smallBreak && (
<div className={`${baseClass}__list-selection`}>
<ListSelection
collectionConfig={collectionConfig}
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
label={getTranslation(collectionConfig.labels.plural, i18n)}
/>
<div className={`${baseClass}__list-selection-actions`}>
@@ -278,14 +277,6 @@ export function DefaultListView(props: ListViewClientProps) {
]
: [<SelectMany key="select-many" onClick={onBulkSelect} />]
: beforeActions}
{!disableBulkEdit && (
<Fragment>
<EditMany collection={collectionConfig} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
</Fragment>
)}
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
</div>
</div>
)}

View File

@@ -12,6 +12,7 @@ import {
openNav,
saveDocAndAssert,
saveDocHotkeyAndAssert,
// throttleTest,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
@@ -51,7 +52,6 @@ let payload: PayloadTestSDK<Config>
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import { openDocControls } from 'helpers/e2e/openDocControls.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
@@ -101,6 +101,12 @@ describe('General', () => {
})
beforeEach(async () => {
// await throttleTest({
// page,
// context,
// delay: 'Fast 4G',
// })
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests',
@@ -735,6 +741,7 @@ describe('General', () => {
await page.goto(postsUrl.list)
await page.locator('#search-filter-input').fill('Post')
await page.waitForURL(/search=Post/)
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
await page.locator('input#select-all').check()
await page.locator('button#select-all-across-pages').click()
@@ -860,9 +867,12 @@ describe('General', () => {
await page.goto(postsUrl.list)
await page.locator('#search-filter-input').fill('Post')
await page.waitForURL(/search=Post/)
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
await page.locator('input#select-all').check()
await page.locator('button#select-all-across-pages').click()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()