feat(ui): adds admin.components.listMenuItems option (#11230)

### What?
Adds new option `admin.components.listMenuItems` to allow custom
components to be injected after the existing list controls in the
collection list view.

### Why?
Needed to facilitate import/export plugin.

#### Testing

Use `pnpm dev admin` to see example component and see test added to
`test/admin/e2e/list-view`.


## Update since feature was reverted
The custom list controls and now rendered with no surrounding padding or
border radius.

<img width="596" alt="Screenshot 2025-02-17 at 5 06 44 PM"
src="https://github.com/user-attachments/assets/57209367-5433-4a4c-8797-0f9671da15c8"
/>

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
Jessica Chowdhury
2025-02-18 14:35:27 +00:00
committed by GitHub
parent 117949b8d9
commit 8a2b712287
49 changed files with 161 additions and 10 deletions

View File

@@ -152,15 +152,16 @@ export const MyCollection: CollectionConfig = {
The following options are available: The following options are available:
| Option | Description | | Option | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `afterList` | An array of components to inject _after_ the built-in List View. [More details](../custom-components/list-view#afterlist). | | `afterList` | An array of components to inject _after_ the built-in List View. [More details](../custom-components/list-view#afterlist). |
| `afterListTable` | An array of components to inject _after_ the built-in List View's table. [More details](../custom-components/list-view#afterlisttable). | | `afterListTable` | An array of components to inject _after_ the built-in List View's table. [More details](../custom-components/list-view#afterlisttable). |
| `beforeList` | An array of components to inject _before_ the built-in List View. [More details](../custom-components/list-view#beforelist). | | `beforeList` | An array of components to inject _before_ the built-in List View. [More details](../custom-components/list-view#beforelist). |
| `beforeListTable` | An array of components to inject _before_ the built-in List View's table. [More details](../custom-components/list-view#beforelisttable). | | `beforeListTable` | An array of components to inject _before_ the built-in List View's table. [More details](../custom-components/list-view#beforelisttable). |
| `Description` | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. [More details](../custom-components/list-view#description). | | `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) |
| `edit` | Override specific components within the Edit View. [More details](#edit-view-options). | | `Description` | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. [More details](../custom-components/list-view#description). |
| `views` | Override or create new views within the Admin Panel. [More details](../custom-components/custom-views). | | `edit` | Override specific components within the Edit View. [More details](#edit-view-options). |
| `views` | Override or create new views within the Admin Panel. [More details](../custom-components/custom-views). |
#### Edit View Options #### Edit View Options

View File

@@ -44,6 +44,18 @@ export const renderListViewSlots = ({
}) })
} }
const listMenuItems = collectionConfig.admin.components?.listMenuItems
if (Array.isArray(listMenuItems)) {
result.listMenuItems = [
RenderServerComponent({
clientProps,
Component: listMenuItems,
importMap: payload.importMap,
serverProps,
}),
]
}
if (collectionConfig.admin.components?.afterListTable) { if (collectionConfig.admin.components?.afterListTable) {
result.AfterListTable = RenderServerComponent({ result.AfterListTable = RenderServerComponent({
clientProps: clientProps satisfies AfterListTableClientProps, clientProps: clientProps satisfies AfterListTableClientProps,

View File

@@ -14,6 +14,7 @@ export type ListViewSlots = {
BeforeList?: React.ReactNode BeforeList?: React.ReactNode
BeforeListTable?: React.ReactNode BeforeListTable?: React.ReactNode
Description?: React.ReactNode Description?: React.ReactNode
listMenuItems?: React.ReactNode[]
Table: React.ReactNode Table: React.ReactNode
} }

View File

@@ -30,6 +30,7 @@ export function iterateCollections({
}) })
addToImportMap(collection.admin?.components?.afterList) addToImportMap(collection.admin?.components?.afterList)
addToImportMap(collection.admin?.components?.listMenuItems)
addToImportMap(collection.admin?.components?.afterListTable) addToImportMap(collection.admin?.components?.afterListTable)
addToImportMap(collection.admin?.components?.beforeList) addToImportMap(collection.admin?.components?.beforeList)
addToImportMap(collection.admin?.components?.beforeListTable) addToImportMap(collection.admin?.components?.beforeListTable)

View File

@@ -304,6 +304,7 @@ export type CollectionAdminOptions = {
*/ */
Upload?: CustomUpload Upload?: CustomUpload
} }
listMenuItems?: CustomComponent[]
views?: { views?: {
/** /**
* Set to a React component to replace the entire Edit View, including all nested routes. * Set to a React component to replace the entire Edit View, including all nested routes.

View File

@@ -209,6 +209,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:loading', 'general:loading',
'general:locale', 'general:locale',
'general:menu', 'general:menu',
'general:moreOptions',
'general:moveDown', 'general:moveDown',
'general:moveUp', 'general:moveUp',
'general:next', 'general:next',

View File

@@ -266,6 +266,7 @@ export const arTranslations: DefaultTranslationsObject = {
locale: 'اللّغة', locale: 'اللّغة',
locales: 'اللّغات', locales: 'اللّغات',
menu: 'قائمة', menu: 'قائمة',
moreOptions: 'خيارات أكثر',
moveDown: 'التّحريك إلى الأسفل', moveDown: 'التّحريك إلى الأسفل',
moveUp: 'التّحريك إلى الأعلى', moveUp: 'التّحريك إلى الأعلى',
newPassword: 'كلمة مرور جديدة', newPassword: 'كلمة مرور جديدة',

View File

@@ -270,6 +270,7 @@ export const azTranslations: DefaultTranslationsObject = {
locale: 'Lokal', locale: 'Lokal',
locales: 'Dillər', locales: 'Dillər',
menu: 'Menyu', menu: 'Menyu',
moreOptions: 'Daha çox seçimlər',
moveDown: 'Aşağı hərəkət et', moveDown: 'Aşağı hərəkət et',
moveUp: 'Yuxarı hərəkət et', moveUp: 'Yuxarı hərəkət et',
newPassword: 'Yeni şifrə', newPassword: 'Yeni şifrə',

View File

@@ -269,6 +269,7 @@ export const bgTranslations: DefaultTranslationsObject = {
locale: 'Локализация', locale: 'Локализация',
locales: 'Локализации', locales: 'Локализации',
menu: 'Меню', menu: 'Меню',
moreOptions: 'Повече опции',
moveDown: 'Надолу', moveDown: 'Надолу',
moveUp: 'Нагоре', moveUp: 'Нагоре',
newPassword: 'Нова парола', newPassword: 'Нова парола',

View File

@@ -270,6 +270,7 @@ export const caTranslations: DefaultTranslationsObject = {
locale: 'Idioma', locale: 'Idioma',
locales: 'Idiomes', locales: 'Idiomes',
menu: 'Menu', menu: 'Menu',
moreOptions: 'Més opcions',
moveDown: 'Mou avall', moveDown: 'Mou avall',
moveUp: 'Move amunt', moveUp: 'Move amunt',
newPassword: 'Nova contrasenya', newPassword: 'Nova contrasenya',

View File

@@ -268,6 +268,7 @@ export const csTranslations: DefaultTranslationsObject = {
locale: 'Místní verze', locale: 'Místní verze',
locales: 'Lokality', locales: 'Lokality',
menu: 'Jídelní lístek', menu: 'Jídelní lístek',
moreOptions: 'Více možností',
moveDown: 'Posunout dolů', moveDown: 'Posunout dolů',
moveUp: 'Posunout nahoru', moveUp: 'Posunout nahoru',
newPassword: 'Nové heslo', newPassword: 'Nové heslo',

View File

@@ -268,6 +268,7 @@ export const daTranslations: DefaultTranslationsObject = {
locale: 'Lokalitet', locale: 'Lokalitet',
locales: 'Lokaliteter', locales: 'Lokaliteter',
menu: 'Menu', menu: 'Menu',
moreOptions: 'Flere muligheder',
moveDown: 'Ryk ned', moveDown: 'Ryk ned',
moveUp: 'Ryk op', moveUp: 'Ryk op',
newPassword: 'Ny adgangskode', newPassword: 'Ny adgangskode',

View File

@@ -274,6 +274,7 @@ export const deTranslations: DefaultTranslationsObject = {
locale: 'Sprache', locale: 'Sprache',
locales: 'Sprachen', locales: 'Sprachen',
menu: 'Menü', menu: 'Menü',
moreOptions: 'Mehr Optionen',
moveDown: 'Nach unten bewegen', moveDown: 'Nach unten bewegen',
moveUp: 'Nach oben bewegen', moveUp: 'Nach oben bewegen',
newPassword: 'Neues Passwort', newPassword: 'Neues Passwort',

View File

@@ -270,6 +270,7 @@ export const enTranslations = {
locale: 'Locale', locale: 'Locale',
locales: 'Locales', locales: 'Locales',
menu: 'Menu', menu: 'Menu',
moreOptions: 'More options',
moveDown: 'Move Down', moveDown: 'Move Down',
moveUp: 'Move Up', moveUp: 'Move Up',
newPassword: 'New Password', newPassword: 'New Password',

View File

@@ -274,6 +274,7 @@ export const esTranslations: DefaultTranslationsObject = {
locale: 'Regional', locale: 'Regional',
locales: 'Locales', locales: 'Locales',
menu: 'Menú', menu: 'Menú',
moreOptions: 'Más opciones',
moveDown: 'Mover abajo', moveDown: 'Mover abajo',
moveUp: 'Mover arriba', moveUp: 'Mover arriba',
newPassword: 'Nueva contraseña', newPassword: 'Nueva contraseña',

View File

@@ -267,6 +267,7 @@ export const etTranslations: DefaultTranslationsObject = {
locale: 'Keel', locale: 'Keel',
locales: 'Keeled', locales: 'Keeled',
menu: 'Menüü', menu: 'Menüü',
moreOptions: 'Rohkem valikuid',
moveDown: 'Liiguta alla', moveDown: 'Liiguta alla',
moveUp: 'Liiguta üles', moveUp: 'Liiguta üles',
newPassword: 'Uus parool', newPassword: 'Uus parool',

View File

@@ -268,6 +268,7 @@ export const faTranslations: DefaultTranslationsObject = {
locale: 'زبان', locale: 'زبان',
locales: 'زبان‌ها', locales: 'زبان‌ها',
menu: 'منو', menu: 'منو',
moreOptions: 'گزینه‌های بیشتر',
moveDown: 'حرکت به پایین', moveDown: 'حرکت به پایین',
moveUp: 'حرکت به بالا', moveUp: 'حرکت به بالا',
newPassword: 'گذرواژه تازه', newPassword: 'گذرواژه تازه',

View File

@@ -277,6 +277,7 @@ export const frTranslations: DefaultTranslationsObject = {
locale: 'Paramètres régionaux', locale: 'Paramètres régionaux',
locales: 'Paramètres régionaux', locales: 'Paramètres régionaux',
menu: 'Menu', menu: 'Menu',
moreOptions: "Plus d'options",
moveDown: 'Déplacer vers le bas', moveDown: 'Déplacer vers le bas',
moveUp: 'Déplacer vers le haut', moveUp: 'Déplacer vers le haut',
newPassword: 'Nouveau mot de passe', newPassword: 'Nouveau mot de passe',

View File

@@ -264,6 +264,7 @@ export const heTranslations: DefaultTranslationsObject = {
locale: 'שפה', locale: 'שפה',
locales: 'שפות', locales: 'שפות',
menu: 'תפריט', menu: 'תפריט',
moreOptions: 'אפשרויות נוספות',
moveDown: 'הזז למטה', moveDown: 'הזז למטה',
moveUp: 'הזז למעלה', moveUp: 'הזז למעלה',
newPassword: 'סיסמה חדשה', newPassword: 'סיסמה חדשה',

View File

@@ -270,6 +270,7 @@ export const hrTranslations: DefaultTranslationsObject = {
locale: 'Jezik', locale: 'Jezik',
locales: 'Prijevodi', locales: 'Prijevodi',
menu: 'Izbornik', menu: 'Izbornik',
moreOptions: 'Više opcija',
moveDown: 'Pomakni dolje', moveDown: 'Pomakni dolje',
moveUp: 'Pomakni gore', moveUp: 'Pomakni gore',
newPassword: 'Nova lozinka', newPassword: 'Nova lozinka',

View File

@@ -272,6 +272,7 @@ export const huTranslations: DefaultTranslationsObject = {
locale: 'Nyelv', locale: 'Nyelv',
locales: 'Nyelvek', locales: 'Nyelvek',
menu: 'Menü', menu: 'Menü',
moreOptions: 'Több opció',
moveDown: 'Mozgatás lefelé', moveDown: 'Mozgatás lefelé',
moveUp: 'Mozgatás felfelé', moveUp: 'Mozgatás felfelé',
newPassword: 'Új jelszó', newPassword: 'Új jelszó',

View File

@@ -273,6 +273,7 @@ export const itTranslations: DefaultTranslationsObject = {
locale: 'Locale', locale: 'Locale',
locales: 'Localizzazioni', locales: 'Localizzazioni',
menu: 'Menù', menu: 'Menù',
moreOptions: 'Più opzioni',
moveDown: 'Sposta sotto', moveDown: 'Sposta sotto',
moveUp: 'Sposta sopra', moveUp: 'Sposta sopra',
newPassword: 'Nuova Password', newPassword: 'Nuova Password',

View File

@@ -270,6 +270,7 @@ export const jaTranslations: DefaultTranslationsObject = {
locale: 'ロケール', locale: 'ロケール',
locales: 'ロケール', locales: 'ロケール',
menu: 'メニュー', menu: 'メニュー',
moreOptions: 'より多くのオプション',
moveDown: '下へ移動', moveDown: '下へ移動',
moveUp: '上へ移動', moveUp: '上へ移動',
newPassword: '新しいパスワード', newPassword: '新しいパスワード',

View File

@@ -268,6 +268,7 @@ export const koTranslations: DefaultTranslationsObject = {
locale: 'locale', locale: 'locale',
locales: 'locale', locales: 'locale',
menu: '메뉴', menu: '메뉴',
moreOptions: '더 많은 옵션',
moveDown: '아래로 이동', moveDown: '아래로 이동',
moveUp: '위로 이동', moveUp: '위로 이동',
newPassword: '새 비밀번호', newPassword: '새 비밀번호',

View File

@@ -272,6 +272,7 @@ export const myTranslations: DefaultTranslationsObject = {
locale: 'ဒေသ', locale: 'ဒေသ',
locales: 'Locales', locales: 'Locales',
menu: 'မီနူး', menu: 'မီနူး',
moreOptions: 'ပိုမိုများစွာရွေးချယ်ခွင့်',
moveDown: 'Move Down', moveDown: 'Move Down',
moveUp: 'Move Up', moveUp: 'Move Up',
newPassword: 'စကားဝှက် အသစ်', newPassword: 'စကားဝှက် အသစ်',

View File

@@ -270,6 +270,7 @@ export const nbTranslations: DefaultTranslationsObject = {
locale: 'Lokalitet', locale: 'Lokalitet',
locales: 'Språk', locales: 'Språk',
menu: 'Meny', menu: 'Meny',
moreOptions: 'Flere alternativer',
moveDown: 'Flytt ned', moveDown: 'Flytt ned',
moveUp: 'Flytt opp', moveUp: 'Flytt opp',
newPassword: 'Nytt passord', newPassword: 'Nytt passord',

View File

@@ -273,6 +273,7 @@ export const nlTranslations: DefaultTranslationsObject = {
locale: 'Taal', locale: 'Taal',
locales: 'Landinstellingen', locales: 'Landinstellingen',
menu: 'Menu', menu: 'Menu',
moreOptions: 'Meer opties',
moveDown: 'Verplaats naar beneden', moveDown: 'Verplaats naar beneden',
moveUp: 'Verplaats naar boven', moveUp: 'Verplaats naar boven',
newPassword: 'Nieuw wachtwoord', newPassword: 'Nieuw wachtwoord',

View File

@@ -270,6 +270,7 @@ export const plTranslations: DefaultTranslationsObject = {
locale: 'Ustawienia regionalne', locale: 'Ustawienia regionalne',
locales: 'Ustawienia regionalne', locales: 'Ustawienia regionalne',
menu: 'Menu', menu: 'Menu',
moreOptions: 'Więcej opcji',
moveDown: 'Przesuń niżej', moveDown: 'Przesuń niżej',
moveUp: 'Przesuń wyżej', moveUp: 'Przesuń wyżej',
newPassword: 'Nowe hasło', newPassword: 'Nowe hasło',

View File

@@ -270,6 +270,7 @@ export const ptTranslations: DefaultTranslationsObject = {
locale: 'Local', locale: 'Local',
locales: 'Localizações', locales: 'Localizações',
menu: 'Cardápio', menu: 'Cardápio',
moreOptions: 'Mais opções',
moveDown: 'Mover para Baixo', moveDown: 'Mover para Baixo',
moveUp: 'Mover para Cima', moveUp: 'Mover para Cima',
newPassword: 'Nova Senha', newPassword: 'Nova Senha',

View File

@@ -274,6 +274,7 @@ export const roTranslations: DefaultTranslationsObject = {
locale: 'Localitate', locale: 'Localitate',
locales: 'Localuri', locales: 'Localuri',
menu: 'Meniu', menu: 'Meniu',
moreOptions: 'Mai multe opțiuni',
moveDown: 'Mutați în jos', moveDown: 'Mutați în jos',
moveUp: 'Mutați în sus', moveUp: 'Mutați în sus',
newPassword: 'Parolă nouă', newPassword: 'Parolă nouă',

View File

@@ -270,6 +270,7 @@ export const rsTranslations: DefaultTranslationsObject = {
locale: 'Језик', locale: 'Језик',
locales: 'Преводи', locales: 'Преводи',
menu: 'Мени', menu: 'Мени',
moreOptions: 'Više opcija',
moveDown: 'Помери доле', moveDown: 'Помери доле',
moveUp: 'Помери горе', moveUp: 'Помери горе',
newPassword: 'Нова лозинка', newPassword: 'Нова лозинка',

View File

@@ -270,6 +270,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
locale: 'Jezik', locale: 'Jezik',
locales: 'Prevodi', locales: 'Prevodi',
menu: 'Meni', menu: 'Meni',
moreOptions: 'Više opcija',
moveDown: 'Pomeri dole', moveDown: 'Pomeri dole',
moveUp: 'Pomeri gore', moveUp: 'Pomeri gore',
newPassword: 'Nova lozinka', newPassword: 'Nova lozinka',

View File

@@ -272,6 +272,7 @@ export const ruTranslations: DefaultTranslationsObject = {
locale: 'Локаль', locale: 'Локаль',
locales: 'Локали', locales: 'Локали',
menu: 'Меню', menu: 'Меню',
moreOptions: 'Больше вариантов',
moveDown: 'Сдвинуть вниз', moveDown: 'Сдвинуть вниз',
moveUp: 'Сдвинуть вверх', moveUp: 'Сдвинуть вверх',
newPassword: 'Новый пароль', newPassword: 'Новый пароль',

View File

@@ -271,6 +271,7 @@ export const skTranslations: DefaultTranslationsObject = {
locale: 'Jazyk', locale: 'Jazyk',
locales: 'Jazyky', locales: 'Jazyky',
menu: 'Menu', menu: 'Menu',
moreOptions: 'Viac možností',
moveDown: 'Presunúť dolu', moveDown: 'Presunúť dolu',
moveUp: 'Presunúť hore', moveUp: 'Presunúť hore',
newPassword: 'Nové heslo', newPassword: 'Nové heslo',

View File

@@ -269,6 +269,7 @@ export const slTranslations: DefaultTranslationsObject = {
locale: 'Jezik', locale: 'Jezik',
locales: 'Jeziki', locales: 'Jeziki',
menu: 'Meni', menu: 'Meni',
moreOptions: 'Več možnosti',
moveDown: 'Premakni dol', moveDown: 'Premakni dol',
moveUp: 'Premakni gor', moveUp: 'Premakni gor',
newPassword: 'Novo geslo', newPassword: 'Novo geslo',

View File

@@ -270,6 +270,7 @@ export const svTranslations: DefaultTranslationsObject = {
locale: 'Lokal', locale: 'Lokal',
locales: 'Språk', locales: 'Språk',
menu: 'Meny', menu: 'Meny',
moreOptions: 'Fler alternativ',
moveDown: 'Flytta Ner', moveDown: 'Flytta Ner',
moveUp: 'Flytta Upp', moveUp: 'Flytta Upp',
newPassword: 'Nytt Lösenord', newPassword: 'Nytt Lösenord',

View File

@@ -266,6 +266,7 @@ export const thTranslations: DefaultTranslationsObject = {
locale: 'ตำแหน่งที่ตั้ง', locale: 'ตำแหน่งที่ตั้ง',
locales: 'ภาษา', locales: 'ภาษา',
menu: 'เมนู', menu: 'เมนู',
moreOptions: 'ตัวเลือกเพิ่มเติม',
moveDown: 'ขยับขึ้น', moveDown: 'ขยับขึ้น',
moveUp: 'ขยับลง', moveUp: 'ขยับลง',
newPassword: 'รหัสผ่านใหม่', newPassword: 'รหัสผ่านใหม่',

View File

@@ -273,6 +273,7 @@ export const trTranslations: DefaultTranslationsObject = {
locale: 'Yerel ayar', locale: 'Yerel ayar',
locales: 'Diller', locales: 'Diller',
menu: 'Menü', menu: 'Menü',
moreOptions: 'Daha fazla seçenek',
moveDown: 'Aşağı taşı', moveDown: 'Aşağı taşı',
moveUp: 'Yukarı taşı', moveUp: 'Yukarı taşı',
newPassword: 'Yeni parola', newPassword: 'Yeni parola',

View File

@@ -269,6 +269,7 @@ export const ukTranslations: DefaultTranslationsObject = {
locale: 'Локаль', locale: 'Локаль',
locales: 'Локалі', locales: 'Локалі',
menu: 'Меню', menu: 'Меню',
moreOptions: 'Більше варіантів',
moveDown: 'Перемістити нижче', moveDown: 'Перемістити нижче',
moveUp: 'Перемістити вище', moveUp: 'Перемістити вище',
newPassword: 'Новий пароль', newPassword: 'Новий пароль',

View File

@@ -269,6 +269,7 @@ export const viTranslations: DefaultTranslationsObject = {
locale: 'Ngôn ngữ', locale: 'Ngôn ngữ',
locales: 'Khu vực', locales: 'Khu vực',
menu: 'Thực đơn', menu: 'Thực đơn',
moreOptions: 'Nhiều lựa chọn hơn',
moveDown: 'Di chuyển xuống', moveDown: 'Di chuyển xuống',
moveUp: 'Di chuyển lên', moveUp: 'Di chuyển lên',
newPassword: 'Mật khảu mới', newPassword: 'Mật khảu mới',

View File

@@ -260,6 +260,7 @@ export const zhTranslations: DefaultTranslationsObject = {
locale: '语言环境', locale: '语言环境',
locales: '语言环境', locales: '语言环境',
menu: '菜单', menu: '菜单',
moreOptions: '更多选项',
moveDown: '向下移动', moveDown: '向下移动',
moveUp: '向上移动', moveUp: '向上移动',
newPassword: '新密码', newPassword: '新密码',

View File

@@ -260,6 +260,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
locale: '語言環境', locale: '語言環境',
locales: '語言環境', locales: '語言環境',
menu: '菜單', menu: '菜單',
moreOptions: '更多選項',
moveDown: '向下移動', moveDown: '向下移動',
moveUp: '向上移動', moveUp: '向上移動',
newPassword: '新密碼', newPassword: '新密碼',

View File

@@ -19,6 +19,11 @@
} }
} }
&__custom-control {
padding: 0;
border-radius: 0;
}
&__buttons-wrap { &__buttons-wrap {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -5,8 +5,10 @@ import { useWindowInfo } from '@faceless-ui/window-info'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import React, { Fragment, useEffect, useRef, useState } from 'react' import React, { Fragment, useEffect, useRef, useState } from 'react'
import { Popup, PopupList } from '../../elements/Popup/index.js'
import { useUseTitleField } from '../../hooks/useUseAsTitle.js' import { useUseTitleField } from '../../hooks/useUseAsTitle.js'
import { ChevronIcon } from '../../icons/Chevron/index.js' import { ChevronIcon } from '../../icons/Chevron/index.js'
import { Dots } from '../../icons/Dots/index.js'
import { SearchIcon } from '../../icons/Search/index.js' import { SearchIcon } from '../../icons/Search/index.js'
import { useListQuery } from '../../providers/ListQuery/index.js' import { useListQuery } from '../../providers/ListQuery/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
@@ -36,6 +38,7 @@ export type ListControlsProps = {
readonly handleSearchChange?: (search: string) => void readonly handleSearchChange?: (search: string) => void
readonly handleSortChange?: (sort: string) => void readonly handleSortChange?: (sort: string) => void
readonly handleWhereChange?: (where: Where) => void readonly handleWhereChange?: (where: Where) => void
readonly listMenuItems?: React.ReactNode[]
readonly renderedFilters?: Map<string, React.ReactNode> readonly renderedFilters?: Map<string, React.ReactNode>
readonly resolvedFilterOptions?: Map<string, ResolvedFilterOptions> readonly resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
} }
@@ -54,10 +57,10 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
disableBulkEdit, disableBulkEdit,
enableColumns = true, enableColumns = true,
enableSort = false, enableSort = false,
listMenuItems,
renderedFilters, renderedFilters,
resolvedFilterOptions, resolvedFilterOptions,
} = props } = props
const { handleSearchChange, query } = useListQuery() const { handleSearchChange, query } = useListQuery()
const titleField = useUseTitleField(collectionConfig) const titleField = useUseTitleField(collectionConfig)
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
@@ -194,6 +197,17 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
{t('general:sort')} {t('general:sort')}
</Pill> </Pill>
)} )}
{listMenuItems && (
<Popup
button={<Dots ariaLabel={t('general:moreOptions')} />}
className={`${baseClass}__popup`}
horizontalAlign="right"
size="large"
verticalAlign="bottom"
>
<PopupList.ButtonGroup>{listMenuItems.map((item) => item)}</PopupList.ButtonGroup>
</Popup>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,27 @@
@import '../../scss/styles';
@layer payload-default {
.dots {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 2px;
background-color: var(--theme-elevation-150);
border-radius: $style-radius-m;
height: calc(var(--base) * 1.2);
width: calc(var(--base) * 1.2);
&:hover {
background-color: var(--theme-elevation-100);
}
> div {
width: 2.5px;
height: 2.5px;
border-radius: 100%;
background-color: currentColor;
}
}
}

View File

@@ -0,0 +1,17 @@
import React from 'react'
import './index.scss'
export const Dots: React.FC<{ ariaLabel?: string; className?: string }> = ({
ariaLabel,
className,
}) => (
<div
aria-label={ariaLabel}
className={[className && className, 'dots'].filter(Boolean).join(' ')}
>
<div />
<div />
<div />
</div>
)

View File

@@ -52,6 +52,7 @@ export function DefaultListView(props: ListViewClientProps) {
disableBulkEdit, disableBulkEdit,
enableRowSelections, enableRowSelections,
hasCreatePermission, hasCreatePermission,
listMenuItems,
listPreferences, listPreferences,
newDocumentURL, newDocumentURL,
preferenceKey, preferenceKey,
@@ -195,6 +196,7 @@ export function DefaultListView(props: ListViewClientProps) {
collectionSlug={collectionSlug} collectionSlug={collectionSlug}
disableBulkDelete={disableBulkDelete} disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit} disableBulkEdit={disableBulkEdit}
listMenuItems={listMenuItems}
renderedFilters={renderedFilters} renderedFilters={renderedFilters}
resolvedFilterOptions={resolvedFilterOptions} resolvedFilterOptions={resolvedFilterOptions}
/> />

View File

@@ -33,6 +33,26 @@ export const Posts: CollectionConfig = {
}, },
}, },
], ],
listMenuItems: [
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'listMenuItems',
},
},
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'Many of them',
},
},
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'Ok last one',
},
},
],
afterList: [ afterList: [
{ {
path: '/components/Banner/index.js#Banner', path: '/components/Banner/index.js#Banner',

View File

@@ -198,6 +198,19 @@ describe('List View', () => {
).toBeVisible() ).toBeVisible()
}) })
test('should render custom listMenuItems component', async () => {
await page.goto(postsUrl.list)
const kebabMenu = page.locator('.list-controls__popup')
await expect(kebabMenu).toBeVisible()
await kebabMenu.click()
await expect(
page.locator('.popup-button-list').locator('div', {
hasText: 'listMenuItems',
}),
).toBeVisible()
})
test('should render custom afterListTable component', async () => { test('should render custom afterListTable component', async () => {
await page.goto(postsUrl.list) await page.goto(postsUrl.list)
await expect( await expect(