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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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,21 +35,27 @@ 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 && (
|
||||
<button
|
||||
aria-label={t('general:selectAll', { count, label })}
|
||||
className={`${baseClass}__button`}
|
||||
id="select-all-across-pages"
|
||||
onClick={() => toggleAll(true)}
|
||||
type="button"
|
||||
>
|
||||
{t('general:selectAll', { count: totalDocs, label: '' })}
|
||||
</button>
|
||||
)}
|
||||
{!disableBulkEdit && !disableBulkDelete && <span>—</span>}
|
||||
{!disableBulkEdit && (
|
||||
<Fragment>
|
||||
<span>—</span>
|
||||
<button
|
||||
aria-label={t('general:selectAll', { count, label })}
|
||||
className={`${baseClass}__button`}
|
||||
id="select-all-across-pages"
|
||||
onClick={() => toggleAll(true)}
|
||||
type="button"
|
||||
>
|
||||
{t('general:selectAll', { count: totalDocs, label })}
|
||||
</button>
|
||||
<EditMany collection={collectionConfig} />
|
||||
<PublishMany collection={collectionConfig} />
|
||||
<UnpublishMany collection={collectionConfig} />
|
||||
</Fragment>
|
||||
)}
|
||||
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
21
packages/ui/src/elements/PublishMany/index.scss
Normal file
21
packages/ui/src/elements/PublishMany/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')}
|
||||
|
||||
21
packages/ui/src/elements/UnpublishMany/index.scss
Normal file
21
packages/ui/src/elements/UnpublishMany/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user