From 7277f17f14f9ccb4ac76f433539f6e389944dfed Mon Sep 17 00:00:00 2001
From: Jessica Chowdhury <67977755+JessChowdhury@users.noreply.github.com>
Date: Thu, 6 Feb 2025 23:24:04 +0000
Subject: [PATCH] feat(ui): adds admin.components.listControlsMenu option
(#10981)
### What?
Adds new option `admin.components.listControlsMenu` to allow custom
components to be injected after the existing list controls in the
collection list view.
### Why?
Needed to facilitate import/export plugin.
#### Preview & Testing
Use `pnpm dev admin` to see example component and see test added to
`test/admin/e2e/list-view`.
---------
Co-authored-by: Dan Ribbens
---
docs/configuration/collections.mdx | 1 +
.../src/views/List/renderListViewSlots.tsx | 9 +++++++
.../generateImportMap/iterateCollections.ts | 1 +
.../payload/src/collections/config/types.ts | 1 +
packages/translations/src/clientKeys.ts | 1 +
packages/translations/src/languages/ar.ts | 1 +
packages/translations/src/languages/az.ts | 1 +
packages/translations/src/languages/bg.ts | 1 +
packages/translations/src/languages/ca.ts | 1 +
packages/translations/src/languages/cs.ts | 1 +
packages/translations/src/languages/da.ts | 1 +
packages/translations/src/languages/de.ts | 1 +
packages/translations/src/languages/en.ts | 1 +
packages/translations/src/languages/es.ts | 1 +
packages/translations/src/languages/et.ts | 1 +
packages/translations/src/languages/fa.ts | 1 +
packages/translations/src/languages/fr.ts | 1 +
packages/translations/src/languages/he.ts | 1 +
packages/translations/src/languages/hr.ts | 1 +
packages/translations/src/languages/hu.ts | 1 +
packages/translations/src/languages/it.ts | 1 +
packages/translations/src/languages/ja.ts | 1 +
packages/translations/src/languages/ko.ts | 1 +
packages/translations/src/languages/my.ts | 1 +
packages/translations/src/languages/nb.ts | 1 +
packages/translations/src/languages/nl.ts | 1 +
packages/translations/src/languages/pl.ts | 1 +
packages/translations/src/languages/pt.ts | 1 +
packages/translations/src/languages/ro.ts | 1 +
packages/translations/src/languages/rs.ts | 1 +
.../translations/src/languages/rsLatin.ts | 1 +
packages/translations/src/languages/ru.ts | 1 +
packages/translations/src/languages/sk.ts | 1 +
packages/translations/src/languages/sl.ts | 1 +
packages/translations/src/languages/sv.ts | 1 +
packages/translations/src/languages/th.ts | 1 +
packages/translations/src/languages/tr.ts | 1 +
packages/translations/src/languages/uk.ts | 1 +
packages/translations/src/languages/vi.ts | 1 +
packages/translations/src/languages/zh.ts | 1 +
packages/translations/src/languages/zhTw.ts | 1 +
.../ui/src/elements/ListControls/index.tsx | 21 ++++++++++++++-
packages/ui/src/icons/Dots/index.scss | 27 +++++++++++++++++++
packages/ui/src/icons/Dots/index.tsx | 17 ++++++++++++
packages/ui/src/views/List/index.tsx | 3 +++
test/admin/collections/Posts.ts | 20 ++++++++++++++
test/admin/e2e/list-view/e2e.spec.ts | 13 +++++++++
47 files changed, 149 insertions(+), 1 deletion(-)
create mode 100644 packages/ui/src/icons/Dots/index.scss
create mode 100644 packages/ui/src/icons/Dots/index.tsx
diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx
index 8416e77292..cc58c77188 100644
--- a/docs/configuration/collections.mdx
+++ b/docs/configuration/collections.mdx
@@ -158,6 +158,7 @@ The following options are available:
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
| **`afterList`** | An array of components to inject _after_ the built-in List View |
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
+| **`listControlsMenu`** | An array of components to render as buttons within a menu next to the List Controls (after the Columns and Filters options) |
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
diff --git a/packages/next/src/views/List/renderListViewSlots.tsx b/packages/next/src/views/List/renderListViewSlots.tsx
index 48419a1333..5761b33605 100644
--- a/packages/next/src/views/List/renderListViewSlots.tsx
+++ b/packages/next/src/views/List/renderListViewSlots.tsx
@@ -33,6 +33,15 @@ export const renderListViewSlots = ({
})
}
+ if (collectionConfig.admin.components?.listControlsMenu) {
+ result.ListControlsMenu = RenderServerComponent({
+ clientProps,
+ Component: collectionConfig.admin.components.listControlsMenu,
+ importMap: payload.importMap,
+ serverProps,
+ })
+ }
+
if (collectionConfig.admin.components?.afterListTable) {
result.AfterListTable = RenderServerComponent({
clientProps,
diff --git a/packages/payload/src/bin/generateImportMap/iterateCollections.ts b/packages/payload/src/bin/generateImportMap/iterateCollections.ts
index f6a879d037..9b337f266d 100644
--- a/packages/payload/src/bin/generateImportMap/iterateCollections.ts
+++ b/packages/payload/src/bin/generateImportMap/iterateCollections.ts
@@ -30,6 +30,7 @@ export function iterateCollections({
})
addToImportMap(collection.admin?.components?.afterList)
+ addToImportMap(collection.admin?.components?.listControlsMenu)
addToImportMap(collection.admin?.components?.afterListTable)
addToImportMap(collection.admin?.components?.beforeList)
addToImportMap(collection.admin?.components?.beforeListTable)
diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts
index eafa2be926..a3f4c722f8 100644
--- a/packages/payload/src/collections/config/types.ts
+++ b/packages/payload/src/collections/config/types.ts
@@ -310,6 +310,7 @@ export type CollectionAdminOptions = {
*/
Upload?: CustomUpload
}
+ listControlsMenu?: CustomComponent[]
views?: {
/**
* Set to a React component to replace the entire Edit View, including all nested routes.
diff --git a/packages/translations/src/clientKeys.ts b/packages/translations/src/clientKeys.ts
index c87869f26d..49e63c31c6 100644
--- a/packages/translations/src/clientKeys.ts
+++ b/packages/translations/src/clientKeys.ts
@@ -209,6 +209,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:loading',
'general:locale',
'general:menu',
+ 'general:listControlMenu',
'general:moveDown',
'general:moveUp',
'general:next',
diff --git a/packages/translations/src/languages/ar.ts b/packages/translations/src/languages/ar.ts
index e4f787688d..13624f601e 100644
--- a/packages/translations/src/languages/ar.ts
+++ b/packages/translations/src/languages/ar.ts
@@ -261,6 +261,7 @@ export const arTranslations: DefaultTranslationsObject = {
leaveAnyway: 'المغادرة على أي حال',
leaveWithoutSaving: 'المغادرة بدون حفظ',
light: 'فاتح',
+ listControlMenu: 'قائمة التحكم',
livePreview: 'معاينة مباشرة',
loading: 'يتمّ التّحميل',
locale: 'اللّغة',
diff --git a/packages/translations/src/languages/az.ts b/packages/translations/src/languages/az.ts
index be67f1e3fd..60de39ee57 100644
--- a/packages/translations/src/languages/az.ts
+++ b/packages/translations/src/languages/az.ts
@@ -265,6 +265,7 @@ export const azTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Heç olmasa çıx',
leaveWithoutSaving: 'Saxlamadan çıx',
light: 'Açıq',
+ listControlMenu: 'Siyahı nəzarət menyusu',
livePreview: 'Öncədən baxış',
loading: 'Yüklənir',
locale: 'Lokal',
diff --git a/packages/translations/src/languages/bg.ts b/packages/translations/src/languages/bg.ts
index 3f7b9fc86d..670dd0d3db 100644
--- a/packages/translations/src/languages/bg.ts
+++ b/packages/translations/src/languages/bg.ts
@@ -264,6 +264,7 @@ export const bgTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Напусни въпреки това',
leaveWithoutSaving: 'Напусни без да запазиш',
light: 'Светла',
+ listControlMenu: 'Меню за контрол на списъка',
livePreview: 'Предварителен преглед',
loading: 'Зарежда се',
locale: 'Локализация',
diff --git a/packages/translations/src/languages/ca.ts b/packages/translations/src/languages/ca.ts
index 8598ad19cc..d1b32e2c7e 100644
--- a/packages/translations/src/languages/ca.ts
+++ b/packages/translations/src/languages/ca.ts
@@ -265,6 +265,7 @@ export const caTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Deixa-ho de totes maneres',
leaveWithoutSaving: 'Deixa sense desar',
light: 'Clar',
+ listControlMenu: 'Menú de control de llista',
livePreview: 'Previsualització en viu',
loading: 'Carregant',
locale: 'Idioma',
diff --git a/packages/translations/src/languages/cs.ts b/packages/translations/src/languages/cs.ts
index 82ebcf0daa..2e3365b283 100644
--- a/packages/translations/src/languages/cs.ts
+++ b/packages/translations/src/languages/cs.ts
@@ -263,6 +263,7 @@ export const csTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Přesto odejít',
leaveWithoutSaving: 'Odejít bez uložení',
light: 'Světlé',
+ listControlMenu: 'Nabídka ovládání seznamu',
livePreview: 'Náhled',
loading: 'Načítání',
locale: 'Místní verze',
diff --git a/packages/translations/src/languages/da.ts b/packages/translations/src/languages/da.ts
index e7fccda3d3..3232f7c385 100644
--- a/packages/translations/src/languages/da.ts
+++ b/packages/translations/src/languages/da.ts
@@ -263,6 +263,7 @@ export const daTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Forlad alligevel',
leaveWithoutSaving: 'Forlad uden at gemme',
light: 'Lys',
+ listControlMenu: 'Kontrolmenu for liste',
livePreview: 'Live-forhåndsvisning',
loading: 'Loader',
locale: 'Lokalitet',
diff --git a/packages/translations/src/languages/de.ts b/packages/translations/src/languages/de.ts
index eb19ed4bf4..ad68d0f6cd 100644
--- a/packages/translations/src/languages/de.ts
+++ b/packages/translations/src/languages/de.ts
@@ -269,6 +269,7 @@ export const deTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Trotzdem verlassen',
leaveWithoutSaving: 'Ohne speichern verlassen',
light: 'Hell',
+ listControlMenu: 'Kontrollmenü auflisten',
livePreview: 'Vorschau',
loading: 'Lädt',
locale: 'Sprache',
diff --git a/packages/translations/src/languages/en.ts b/packages/translations/src/languages/en.ts
index 3a0c987691..df1a811af9 100644
--- a/packages/translations/src/languages/en.ts
+++ b/packages/translations/src/languages/en.ts
@@ -265,6 +265,7 @@ export const enTranslations = {
leaveAnyway: 'Leave anyway',
leaveWithoutSaving: 'Leave without saving',
light: 'Light',
+ listControlMenu: 'List control menu',
livePreview: 'Live Preview',
loading: 'Loading',
locale: 'Locale',
diff --git a/packages/translations/src/languages/es.ts b/packages/translations/src/languages/es.ts
index 051e074384..e09c7edeb6 100644
--- a/packages/translations/src/languages/es.ts
+++ b/packages/translations/src/languages/es.ts
@@ -269,6 +269,7 @@ export const esTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Salir de todos modos',
leaveWithoutSaving: 'Salir sin guardar',
light: 'Claro',
+ listControlMenu: 'Menú de control de lista',
livePreview: 'Previsualizar',
loading: 'Cargando',
locale: 'Regional',
diff --git a/packages/translations/src/languages/et.ts b/packages/translations/src/languages/et.ts
index 8c34605bc6..5da5b1615c 100644
--- a/packages/translations/src/languages/et.ts
+++ b/packages/translations/src/languages/et.ts
@@ -262,6 +262,7 @@ export const etTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Lahku ikkagi',
leaveWithoutSaving: 'Lahku ilma salvestamata',
light: 'Hele',
+ listControlMenu: 'Loendikontrolli menüü',
livePreview: 'Reaalajas eelvaade',
loading: 'Laadimine',
locale: 'Keel',
diff --git a/packages/translations/src/languages/fa.ts b/packages/translations/src/languages/fa.ts
index 32c91e6a20..12d4e3fb57 100644
--- a/packages/translations/src/languages/fa.ts
+++ b/packages/translations/src/languages/fa.ts
@@ -263,6 +263,7 @@ export const faTranslations: DefaultTranslationsObject = {
leaveAnyway: 'به هر حال ترک کن',
leaveWithoutSaving: 'ترک کردن بدون ذخیره',
light: 'روشن',
+ listControlMenu: 'منوی کنترل لیست',
livePreview: 'پیشنمایش',
loading: 'در حال بارگذاری',
locale: 'زبان',
diff --git a/packages/translations/src/languages/fr.ts b/packages/translations/src/languages/fr.ts
index 8e27d914e3..12e2b697d5 100644
--- a/packages/translations/src/languages/fr.ts
+++ b/packages/translations/src/languages/fr.ts
@@ -272,6 +272,7 @@ export const frTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Quitter quand même',
leaveWithoutSaving: 'Quitter sans sauvegarder',
light: 'Clair',
+ listControlMenu: 'Menu de contrôle de liste',
livePreview: 'Aperçu',
loading: 'Chargement en cours',
locale: 'Paramètres régionaux',
diff --git a/packages/translations/src/languages/he.ts b/packages/translations/src/languages/he.ts
index 43b6dc3649..ff48978ae8 100644
--- a/packages/translations/src/languages/he.ts
+++ b/packages/translations/src/languages/he.ts
@@ -259,6 +259,7 @@ export const heTranslations: DefaultTranslationsObject = {
leaveAnyway: 'צא בכל זאת',
leaveWithoutSaving: 'צא מבלי לשמור',
light: 'בהיר',
+ listControlMenu: 'תפריט בקרת רשימה',
livePreview: 'תצוגה מקדימה חיה',
loading: 'טוען',
locale: 'שפה',
diff --git a/packages/translations/src/languages/hr.ts b/packages/translations/src/languages/hr.ts
index c261a87b0d..8c9a44732a 100644
--- a/packages/translations/src/languages/hr.ts
+++ b/packages/translations/src/languages/hr.ts
@@ -265,6 +265,7 @@ export const hrTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Svejedno napusti',
leaveWithoutSaving: 'Napusti bez spremanja',
light: 'Svijetlo',
+ listControlMenu: 'Izbornik za kontrolu popisa',
livePreview: 'Pregled',
loading: 'Učitavanje',
locale: 'Jezik',
diff --git a/packages/translations/src/languages/hu.ts b/packages/translations/src/languages/hu.ts
index 3fff9dde93..28c0e449f6 100644
--- a/packages/translations/src/languages/hu.ts
+++ b/packages/translations/src/languages/hu.ts
@@ -267,6 +267,7 @@ export const huTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Távozás mindenképp',
leaveWithoutSaving: 'Távozás mentés nélkül',
light: 'Világos',
+ listControlMenu: 'Lista vezérlő menü',
livePreview: 'Előnézet',
loading: 'Betöltés',
locale: 'Nyelv',
diff --git a/packages/translations/src/languages/it.ts b/packages/translations/src/languages/it.ts
index 793253a53a..c5f47c13b1 100644
--- a/packages/translations/src/languages/it.ts
+++ b/packages/translations/src/languages/it.ts
@@ -268,6 +268,7 @@ export const itTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Esci comunque',
leaveWithoutSaving: 'Esci senza salvare',
light: 'Chiaro',
+ listControlMenu: 'Menu di controllo elenco',
livePreview: 'Anteprima dal vivo',
loading: 'Caricamento',
locale: 'Locale',
diff --git a/packages/translations/src/languages/ja.ts b/packages/translations/src/languages/ja.ts
index e030b98a28..6b2112088d 100644
--- a/packages/translations/src/languages/ja.ts
+++ b/packages/translations/src/languages/ja.ts
@@ -265,6 +265,7 @@ export const jaTranslations: DefaultTranslationsObject = {
leaveAnyway: 'すぐに画面を離れる',
leaveWithoutSaving: '内容が保存されていません',
light: 'ライトモード',
+ listControlMenu: 'リスト制御メニュー',
livePreview: 'プレビュー',
loading: 'ローディング中',
locale: 'ロケール',
diff --git a/packages/translations/src/languages/ko.ts b/packages/translations/src/languages/ko.ts
index 1dfea76687..e982e459e9 100644
--- a/packages/translations/src/languages/ko.ts
+++ b/packages/translations/src/languages/ko.ts
@@ -263,6 +263,7 @@ export const koTranslations: DefaultTranslationsObject = {
leaveAnyway: '그래도 나가시겠습니까?',
leaveWithoutSaving: '저장하지 않고 나가기',
light: '라이트',
+ listControlMenu: '목록 제어 메뉴',
livePreview: '실시간 미리보기',
loading: '불러오는 중',
locale: 'locale',
diff --git a/packages/translations/src/languages/my.ts b/packages/translations/src/languages/my.ts
index 3512b45c73..37c2e3d5b4 100644
--- a/packages/translations/src/languages/my.ts
+++ b/packages/translations/src/languages/my.ts
@@ -267,6 +267,7 @@ export const myTranslations: DefaultTranslationsObject = {
leaveAnyway: 'ဘာဖြစ်ဖြစ် ထွက်မည်။',
leaveWithoutSaving: 'မသိမ်းဘဲ ထွက်မည်။',
light: 'အလင်း',
+ listControlMenu: 'စာရင်းထိန်းချုပ် မီနူး',
livePreview: 'အစမ်းကြည့်ရန်',
loading: 'ဖွင့်နေသည်',
locale: 'ဒေသ',
diff --git a/packages/translations/src/languages/nb.ts b/packages/translations/src/languages/nb.ts
index f3c7076d92..b56324af05 100644
--- a/packages/translations/src/languages/nb.ts
+++ b/packages/translations/src/languages/nb.ts
@@ -265,6 +265,7 @@ export const nbTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Forlat likevel',
leaveWithoutSaving: 'Forlat uten å lagre',
light: 'Lys',
+ listControlMenu: 'Kontrollmeny for liste',
livePreview: 'Forhåndsvisning',
loading: 'Laster',
locale: 'Lokalitet',
diff --git a/packages/translations/src/languages/nl.ts b/packages/translations/src/languages/nl.ts
index 57aebf53b4..de050ec925 100644
--- a/packages/translations/src/languages/nl.ts
+++ b/packages/translations/src/languages/nl.ts
@@ -268,6 +268,7 @@ export const nlTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Toch weggaan',
leaveWithoutSaving: 'Verlaten zonder op te slaan',
light: 'Licht',
+ listControlMenu: 'Controlelijstmenu',
livePreview: 'Voorbeeld',
loading: 'Laden',
locale: 'Taal',
diff --git a/packages/translations/src/languages/pl.ts b/packages/translations/src/languages/pl.ts
index 835f558d06..d3276430d5 100644
--- a/packages/translations/src/languages/pl.ts
+++ b/packages/translations/src/languages/pl.ts
@@ -265,6 +265,7 @@ export const plTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Wyjdź mimo to',
leaveWithoutSaving: 'Wyjdź bez zapisywania',
light: 'Jasny',
+ listControlMenu: 'Menu kontroli listy',
livePreview: 'Podgląd',
loading: 'Ładowanie',
locale: 'Ustawienia regionalne',
diff --git a/packages/translations/src/languages/pt.ts b/packages/translations/src/languages/pt.ts
index 9fc707fceb..512065abb1 100644
--- a/packages/translations/src/languages/pt.ts
+++ b/packages/translations/src/languages/pt.ts
@@ -265,6 +265,7 @@ export const ptTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Sair mesmo assim',
leaveWithoutSaving: 'Sair sem salvar',
light: 'Claro',
+ listControlMenu: 'Menu de controle de lista',
livePreview: 'Pré-visualização',
loading: 'Carregando',
locale: 'Local',
diff --git a/packages/translations/src/languages/ro.ts b/packages/translations/src/languages/ro.ts
index 511287757f..d41edee33e 100644
--- a/packages/translations/src/languages/ro.ts
+++ b/packages/translations/src/languages/ro.ts
@@ -269,6 +269,7 @@ export const roTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Pleacă oricum',
leaveWithoutSaving: 'Plecare fără a salva',
light: 'Light',
+ listControlMenu: 'Meniu de control al listei',
livePreview: 'Previzualizare',
loading: 'Încărcare',
locale: 'Localitate',
diff --git a/packages/translations/src/languages/rs.ts b/packages/translations/src/languages/rs.ts
index cca29ebdbb..f17a516c44 100644
--- a/packages/translations/src/languages/rs.ts
+++ b/packages/translations/src/languages/rs.ts
@@ -265,6 +265,7 @@ export const rsTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Свеједно напусти',
leaveWithoutSaving: 'Напусти без чувања',
light: 'Светло',
+ listControlMenu: 'Meni za kontrolu liste',
livePreview: 'Преглед',
loading: 'Учитавање',
locale: 'Језик',
diff --git a/packages/translations/src/languages/rsLatin.ts b/packages/translations/src/languages/rsLatin.ts
index 98135aa646..c9eb87ca50 100644
--- a/packages/translations/src/languages/rsLatin.ts
+++ b/packages/translations/src/languages/rsLatin.ts
@@ -265,6 +265,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Svejedno napusti',
leaveWithoutSaving: 'Napusti bez čuvanja',
light: 'Svetlo',
+ listControlMenu: 'Kontrolni meni liste',
livePreview: 'Pregled',
loading: 'Učitavanje',
locale: 'Jezik',
diff --git a/packages/translations/src/languages/ru.ts b/packages/translations/src/languages/ru.ts
index dc5fa77cc0..1dabacbc12 100644
--- a/packages/translations/src/languages/ru.ts
+++ b/packages/translations/src/languages/ru.ts
@@ -267,6 +267,7 @@ export const ruTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Все равно уйти',
leaveWithoutSaving: 'Выход без сохранения',
light: 'Светлая',
+ listControlMenu: 'Меню управления списком',
livePreview: 'Предпросмотр',
loading: 'Загрузка',
locale: 'Локаль',
diff --git a/packages/translations/src/languages/sk.ts b/packages/translations/src/languages/sk.ts
index 994bfc8364..c1349f95b3 100644
--- a/packages/translations/src/languages/sk.ts
+++ b/packages/translations/src/languages/sk.ts
@@ -266,6 +266,7 @@ export const skTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Presto odísť',
leaveWithoutSaving: 'Odísť bez uloženia',
light: 'Svetlý',
+ listControlMenu: 'Menu ovládania zoznamu',
livePreview: 'Náhľad',
loading: 'Načítavanie',
locale: 'Jazyk',
diff --git a/packages/translations/src/languages/sl.ts b/packages/translations/src/languages/sl.ts
index e355170978..a0c7ecd6cc 100644
--- a/packages/translations/src/languages/sl.ts
+++ b/packages/translations/src/languages/sl.ts
@@ -264,6 +264,7 @@ export const slTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Vseeno zapusti',
leaveWithoutSaving: 'Zapusti brez shranjevanja',
light: 'Svetlo',
+ listControlMenu: 'Meni za nadzor seznama',
livePreview: 'Predogled',
loading: 'Nalaganje',
locale: 'Jezik',
diff --git a/packages/translations/src/languages/sv.ts b/packages/translations/src/languages/sv.ts
index eb372add21..5e47711794 100644
--- a/packages/translations/src/languages/sv.ts
+++ b/packages/translations/src/languages/sv.ts
@@ -265,6 +265,7 @@ export const svTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Lämna ändå',
leaveWithoutSaving: 'Lämna utan att spara',
light: 'Ljus',
+ listControlMenu: 'Kontrollmeny för lista',
livePreview: 'Förhandsvisa',
loading: 'Läser in',
locale: 'Lokal',
diff --git a/packages/translations/src/languages/th.ts b/packages/translations/src/languages/th.ts
index 9405595a0f..43d703e700 100644
--- a/packages/translations/src/languages/th.ts
+++ b/packages/translations/src/languages/th.ts
@@ -261,6 +261,7 @@ export const thTranslations: DefaultTranslationsObject = {
leaveAnyway: 'ออกจากหน้านี้',
leaveWithoutSaving: 'ออกโดยไม่บันทึก',
light: 'สว่าง',
+ listControlMenu: 'เมนูควบคุมรายการ',
livePreview: 'แสดงตัวอย่าง',
loading: 'กำลังโหลด',
locale: 'ตำแหน่งที่ตั้ง',
diff --git a/packages/translations/src/languages/tr.ts b/packages/translations/src/languages/tr.ts
index 6acb0bf003..969dfa0a28 100644
--- a/packages/translations/src/languages/tr.ts
+++ b/packages/translations/src/languages/tr.ts
@@ -268,6 +268,7 @@ export const trTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Yine de ayrıl',
leaveWithoutSaving: 'Kaydetmeden ayrıl',
light: 'Aydınlık',
+ listControlMenu: 'Liste kontrol menüsü',
livePreview: 'Önizleme',
loading: 'Yükleniyor',
locale: 'Yerel ayar',
diff --git a/packages/translations/src/languages/uk.ts b/packages/translations/src/languages/uk.ts
index 17e379f177..f6a00894bb 100644
--- a/packages/translations/src/languages/uk.ts
+++ b/packages/translations/src/languages/uk.ts
@@ -264,6 +264,7 @@ export const ukTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Все одно вийти',
leaveWithoutSaving: 'Вийти без збереження',
light: 'Світла',
+ listControlMenu: 'Меню контролю списку',
livePreview: 'Попередній перегляд',
loading: 'Завантаження',
locale: 'Локаль',
diff --git a/packages/translations/src/languages/vi.ts b/packages/translations/src/languages/vi.ts
index a9e86a6354..1dc95042e8 100644
--- a/packages/translations/src/languages/vi.ts
+++ b/packages/translations/src/languages/vi.ts
@@ -264,6 +264,7 @@ export const viTranslations: DefaultTranslationsObject = {
leaveAnyway: 'Tiếp tục thoát',
leaveWithoutSaving: 'Thay đổi chưa được lưu',
light: 'Nền sáng',
+ listControlMenu: 'Menu điều khiển danh sách',
livePreview: 'Xem trước',
loading: 'Đang tải',
locale: 'Ngôn ngữ',
diff --git a/packages/translations/src/languages/zh.ts b/packages/translations/src/languages/zh.ts
index dd42c1bbac..ef3ef7f4d9 100644
--- a/packages/translations/src/languages/zh.ts
+++ b/packages/translations/src/languages/zh.ts
@@ -255,6 +255,7 @@ export const zhTranslations: DefaultTranslationsObject = {
leaveAnyway: '无论如何都要离开',
leaveWithoutSaving: '离开而不保存',
light: '亮色',
+ listControlMenu: '列表控制菜单',
livePreview: '预览',
loading: '加载中...',
locale: '语言环境',
diff --git a/packages/translations/src/languages/zhTw.ts b/packages/translations/src/languages/zhTw.ts
index 409680532d..7dedd1443a 100644
--- a/packages/translations/src/languages/zhTw.ts
+++ b/packages/translations/src/languages/zhTw.ts
@@ -255,6 +255,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
leaveAnyway: '無論如何都要離開',
leaveWithoutSaving: '不儲存直接離開',
light: '亮色',
+ listControlMenu: '列表控制菜單',
livePreview: '預覽',
loading: '載入中...',
locale: '語言環境',
diff --git a/packages/ui/src/elements/ListControls/index.tsx b/packages/ui/src/elements/ListControls/index.tsx
index 5011cf8062..faf3bb7432 100644
--- a/packages/ui/src/elements/ListControls/index.tsx
+++ b/packages/ui/src/elements/ListControls/index.tsx
@@ -5,8 +5,10 @@ import { useWindowInfo } from '@faceless-ui/window-info'
import { getTranslation } from '@payloadcms/translations'
import React, { Fragment, useEffect, useRef, useState } from 'react'
+import { Popup, PopupList } from '../../elements/Popup/index.js'
import { useUseTitleField } from '../../hooks/useUseAsTitle.js'
import { ChevronIcon } from '../../icons/Chevron/index.js'
+import { Dots } from '../../icons/Dots/index.js'
import { SearchIcon } from '../../icons/Search/index.js'
import { useListQuery } from '../../providers/ListQuery/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
@@ -19,8 +21,8 @@ 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 './index.scss'
+import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js'
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
const baseClass = 'list-controls'
@@ -36,6 +38,7 @@ export type ListControlsProps = {
readonly handleSearchChange?: (search: string) => void
readonly handleSortChange?: (sort: string) => void
readonly handleWhereChange?: (where: Where) => void
+ readonly listControlsMenu?: React.ReactNode | React.ReactNode[]
readonly renderedFilters?: Map
}
@@ -53,6 +56,7 @@ export const ListControls: React.FC = (props) => {
disableBulkEdit,
enableColumns = true,
enableSort = false,
+ listControlsMenu,
renderedFilters,
} = props
@@ -193,6 +197,21 @@ export const ListControls: React.FC = (props) => {
{t('general:sort')}
)}
+ {listControlsMenu && Array.isArray(listControlsMenu) && (
+ }
+ className={`${baseClass}__popup`}
+ horizontalAlign="right"
+ size="large"
+ verticalAlign="bottom"
+ >
+
+ {listControlsMenu.map((control, index) => (
+ {control}
+ ))}
+
+
+ )}
diff --git a/packages/ui/src/icons/Dots/index.scss b/packages/ui/src/icons/Dots/index.scss
new file mode 100644
index 0000000000..c33d39f9da
--- /dev/null
+++ b/packages/ui/src/icons/Dots/index.scss
@@ -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;
+ }
+ }
+}
diff --git a/packages/ui/src/icons/Dots/index.tsx b/packages/ui/src/icons/Dots/index.tsx
new file mode 100644
index 0000000000..9bea6bf69e
--- /dev/null
+++ b/packages/ui/src/icons/Dots/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import './index.scss'
+
+export const Dots: React.FC<{ ariaLabel?: string; className?: string }> = ({
+ ariaLabel,
+ className,
+}) => (
+
+)
diff --git a/packages/ui/src/views/List/index.tsx b/packages/ui/src/views/List/index.tsx
index 4ef4fda9b9..bbb13c9877 100644
--- a/packages/ui/src/views/List/index.tsx
+++ b/packages/ui/src/views/List/index.tsx
@@ -48,6 +48,7 @@ export type ListViewSlots = {
BeforeList?: React.ReactNode
BeforeListTable?: React.ReactNode
Description?: React.ReactNode
+ ListControlsMenu?: React.ReactNode | React.ReactNode[]
Table: React.ReactNode
}
@@ -79,6 +80,7 @@ export const DefaultListView: React.FC = (props) => {
disableBulkEdit,
enableRowSelections,
hasCreatePermission,
+ ListControlsMenu,
listPreferences,
newDocumentURL,
preferenceKey,
@@ -218,6 +220,7 @@ export const DefaultListView: React.FC = (props) => {
collectionSlug={collectionSlug}
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
+ listControlsMenu={ListControlsMenu}
renderedFilters={renderedFilters}
/>
{BeforeListTable}
diff --git a/test/admin/collections/Posts.ts b/test/admin/collections/Posts.ts
index 3de7b281dc..6e94f753c1 100644
--- a/test/admin/collections/Posts.ts
+++ b/test/admin/collections/Posts.ts
@@ -33,6 +33,26 @@ export const Posts: CollectionConfig = {
},
},
],
+ listControlsMenu: [
+ {
+ path: '/components/Banner/index.js#Banner',
+ clientProps: {
+ message: 'ListControlsMenu',
+ },
+ },
+ {
+ path: '/components/Banner/index.js#Banner',
+ clientProps: {
+ message: 'Many of them',
+ },
+ },
+ {
+ path: '/components/Banner/index.js#Banner',
+ clientProps: {
+ message: 'Ok last one',
+ },
+ },
+ ],
afterList: [
{
path: '/components/Banner/index.js#Banner',
diff --git a/test/admin/e2e/list-view/e2e.spec.ts b/test/admin/e2e/list-view/e2e.spec.ts
index 2ecc2333b7..ca1b39c2d1 100644
--- a/test/admin/e2e/list-view/e2e.spec.ts
+++ b/test/admin/e2e/list-view/e2e.spec.ts
@@ -198,6 +198,19 @@ describe('List View', () => {
).toBeVisible()
})
+ test('should render custom listControlsMenu 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__button').locator('div', {
+ hasText: 'ListControlsMenu',
+ }),
+ ).toBeVisible()
+ })
+
test('should render custom afterListTable component', async () => {
await page.goto(postsUrl.list)
await expect(