@veiag/payload-cmdk 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -304,30 +304,268 @@ Add custom menu items and groups to the command menu.
304
304
  ```typescript
305
305
  {
306
306
  type: 'item',
307
- slug: 'unique-slug',
308
- label: 'Item Label', // Can be localized
309
- icon: 'LucideIconName', // Optional, from lucide.dev/icons
310
- action: {
311
- type: 'link' | 'api',
312
- href: '/path/or/url',
313
- method?: 'GET' | 'POST' | 'PUT' | 'DELETE', // For API actions
314
- body?: { key: 'value' } // For API actions
315
- }
307
+ slug: 'unique-slug', // Must be unique across all items
308
+ label: 'Item Label', // Can be a localized object — see Localization below
309
+ icon: 'LucideIconName', // Optional, from lucide.dev/icons
310
+ collectionSlugs: ['posts'], // Optional — only show on these collection pages
311
+ collectionContext: ['list'], // Optional 'list', 'document', or both
312
+ action: { ... } // See Action Types below
316
313
  }
317
314
  ```
318
315
 
319
316
  #### Custom Menu Group
320
317
 
318
+ Groups are rendered with a heading and can contain multiple items. Groups with identical titles are automatically merged.
319
+
321
320
  ```typescript
322
321
  {
323
322
  type: 'group',
324
- title: 'Group Title', // Can be localized
323
+ title: 'Group Title', // Can be a localized object
324
+ collectionSlugs: ['posts'], // Optional — only show on these collection pages
325
+ collectionContext: ['list'], // Optional
325
326
  items: [
326
327
  // Array of CustomMenuItem
327
328
  ]
328
329
  }
329
330
  ```
330
331
 
332
+ ---
333
+
334
+ #### Action Types
335
+
336
+ Every item requires an `action` that determines what happens when the item is selected.
337
+
338
+ ##### `link` — Navigate to a URL
339
+
340
+ Navigates to a URL. Supports both internal paths and external URLs.
341
+
342
+ ```typescript
343
+ action: {
344
+ type: 'link',
345
+ href: '/admin/collections/posts', // or 'https://your-site.com'
346
+ }
347
+ ```
348
+
349
+ ##### `api` — Call an API endpoint
350
+
351
+ Makes an HTTP request when the item is selected. The menu closes after the request completes (or fails).
352
+
353
+ ```typescript
354
+ action: {
355
+ type: 'api',
356
+ href: '/api/cache/clear',
357
+ method: 'POST', // 'GET' | 'POST' | 'PUT' | 'DELETE' — default: 'GET'
358
+ body: { scope: 'all' }, // Optional JSON body
359
+ }
360
+ ```
361
+
362
+ ##### `function` — Call a client-side handler
363
+
364
+ Because the plugin config is serialized across the Next.js server→client boundary, functions cannot live directly in `payload.config.ts`. Instead, you reference a handler by a **string key** in the config and register the actual function on the client.
365
+
366
+ **Step 1 — reference the key in your config:**
367
+
368
+ ```typescript
369
+ // payload.config.ts
370
+ customItems: [
371
+ {
372
+ type: 'item',
373
+ slug: 'save-document',
374
+ label: 'Save Document',
375
+ icon: 'Save',
376
+ collectionContext: ['document'],
377
+ action: {
378
+ type: 'function',
379
+ key: 'save-current-doc', // Must match the key used in registerCommandMenuAction
380
+ },
381
+ },
382
+ ]
383
+ ```
384
+
385
+ **Step 2 — register the handler in a client component:**
386
+
387
+ ```typescript
388
+ // e.g. app/(payload)/layout.tsx or a custom admin component
389
+ 'use client'
390
+ import { registerCommandMenuAction, unregisterCommandMenuAction } from '@veiag/payload-cmdk/client'
391
+ import { useEffect } from 'react'
392
+
393
+ export default function AdminLayout({ children }) {
394
+ useEffect(() => {
395
+ registerCommandMenuAction('save-current-doc', () => {
396
+ // Submit the currently active form (Payload's save button)
397
+ document.querySelector<HTMLButtonElement>('button[type="submit"]')?.click()
398
+ })
399
+
400
+ // Clean up when the component unmounts
401
+ return () => unregisterCommandMenuAction('save-current-doc')
402
+ }, [])
403
+
404
+ return <>{children}</>
405
+ }
406
+ ```
407
+
408
+ The handler can be synchronous or return a `Promise`. The menu always closes after it resolves (or throws).
409
+
410
+ **Full example with multiple function actions:**
411
+
412
+ ```typescript
413
+ // payload.config.ts
414
+ customItems: [
415
+ {
416
+ type: 'group',
417
+ title: 'Document Actions',
418
+ collectionContext: ['document'],
419
+ items: [
420
+ {
421
+ type: 'item',
422
+ slug: 'save-document',
423
+ label: 'Save Document',
424
+ icon: 'Save',
425
+ action: { type: 'function', key: 'save-current-doc' },
426
+ },
427
+ {
428
+ type: 'item',
429
+ slug: 'copy-doc-link',
430
+ label: 'Copy Document Link',
431
+ icon: 'Link',
432
+ action: { type: 'function', key: 'copy-doc-link' },
433
+ },
434
+ ],
435
+ },
436
+ {
437
+ type: 'item',
438
+ slug: 'import-posts',
439
+ label: 'Import Posts',
440
+ icon: 'Upload',
441
+ collectionSlugs: ['posts'],
442
+ collectionContext: ['list'],
443
+ action: { type: 'function', key: 'import-posts' },
444
+ },
445
+ ]
446
+ ```
447
+
448
+ ```typescript
449
+ // Client component / layout
450
+ 'use client'
451
+ import { registerCommandMenuAction, unregisterCommandMenuAction } from '@veiag/payload-cmdk/client'
452
+ import { useEffect } from 'react'
453
+
454
+ export default function AdminLayout({ children }) {
455
+ useEffect(() => {
456
+ registerCommandMenuAction('save-current-doc', () => {
457
+ document.querySelector<HTMLButtonElement>('button[type="submit"]')?.click()
458
+ })
459
+
460
+ registerCommandMenuAction('copy-doc-link', () => {
461
+ navigator.clipboard.writeText(window.location.href)
462
+ })
463
+
464
+ registerCommandMenuAction('import-posts', async () => {
465
+ await fetch('/api/posts/import', { method: 'POST' })
466
+ })
467
+
468
+ return () => {
469
+ unregisterCommandMenuAction('save-current-doc')
470
+ unregisterCommandMenuAction('copy-doc-link')
471
+ unregisterCommandMenuAction('import-posts')
472
+ }
473
+ }, [])
474
+
475
+ return <>{children}</>
476
+ }
477
+ ```
478
+
479
+ > **Note:** If a key is used in the config but no handler has been registered, the plugin logs a warning to the console and the menu still closes normally.
480
+
481
+ ---
482
+
483
+ #### `collectionSlugs` — Restrict to specific collections
484
+
485
+ Show an item or group only when the user is viewing a particular collection.
486
+
487
+ ```typescript
488
+ {
489
+ type: 'item',
490
+ slug: 'export-posts',
491
+ label: 'Export Posts',
492
+ icon: 'Download',
493
+ collectionSlugs: ['posts'], // only visible on /admin/collections/posts/*
494
+ action: { type: 'function', key: 'export-posts' },
495
+ }
496
+ ```
497
+
498
+ Omit `collectionSlugs` (or pass an empty array) to show the item on all pages, including non-collection pages.
499
+
500
+ ---
501
+
502
+ #### `collectionContext` — Restrict to list or document pages
503
+
504
+ Use `collectionContext` on any item or group to control exactly where inside a collection it appears.
505
+
506
+ - **Type:** `('document' | 'list')[]`
507
+ - **Default:** not set (appears on all pages)
508
+
509
+ | Value | Where it appears |
510
+ |-------|-----------------|
511
+ | `'list'` | Collection list page — e.g. `/admin/collections/posts` |
512
+ | `'document'` | Document edit or create page — e.g. `/admin/collections/posts/abc123` or `/admin/collections/posts/create` |
513
+
514
+ Pass both values, or omit the field entirely, to show the item on all collection pages.
515
+
516
+ `collectionContext` can be combined with `collectionSlugs` to target a specific collection **and** a specific page context, or used alone to apply to all collections.
517
+
518
+ **Examples:**
519
+
520
+ ```typescript
521
+ customItems: [
522
+ // Only on the posts list page
523
+ {
524
+ type: 'item',
525
+ slug: 'import-posts',
526
+ label: 'Import Posts',
527
+ icon: 'Upload',
528
+ collectionSlugs: ['posts'],
529
+ collectionContext: ['list'],
530
+ action: { type: 'function', key: 'import-posts' },
531
+ },
532
+
533
+ // Only when editing or creating a post
534
+ {
535
+ type: 'item',
536
+ slug: 'save-post',
537
+ label: 'Save Post',
538
+ icon: 'Save',
539
+ collectionSlugs: ['posts'],
540
+ collectionContext: ['document'],
541
+ action: { type: 'function', key: 'save-current-doc' },
542
+ },
543
+
544
+ // On any document edit/create page across all collections
545
+ {
546
+ type: 'item',
547
+ slug: 'copy-link',
548
+ label: 'Copy Document Link',
549
+ icon: 'Link',
550
+ collectionContext: ['document'],
551
+ action: { type: 'function', key: 'copy-doc-link' },
552
+ },
553
+
554
+ // On both list and document pages (same as omitting collectionContext)
555
+ {
556
+ type: 'item',
557
+ slug: 'help',
558
+ label: 'Open Help',
559
+ icon: 'HelpCircle',
560
+ collectionSlugs: ['posts'],
561
+ collectionContext: ['list', 'document'],
562
+ action: { type: 'link', href: '/docs' },
563
+ },
564
+ ]
565
+ ```
566
+
567
+ ---
568
+
331
569
  **Example with localization:**
332
570
 
333
571
  ```typescript
@@ -602,3 +840,11 @@ MIT © [VeiaG](https://github.com/VeiaG)
602
840
  - [react-hotkeys-hook Documentation](https://react-hotkeys-hook.vercel.app/docs/intro)
603
841
 
604
842
  # More plugins and payload resources at [PayloadCMS Extensions](https://payload.veiag.dev/)
843
+
844
+ <a href="https://www.star-history.com/#VeiaG/payload-cmdk&type=date&legend=top-left">
845
+ <picture>
846
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=VeiaG/payload-cmdk&type=date&theme=dark&legend=top-left" />
847
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=VeiaG/payload-cmdk&type=date&legend=top-left" />
848
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=VeiaG/payload-cmdk&type=date&legend=top-left" />
849
+ </picture>
850
+ </a>
@@ -1,12 +1,14 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import './modal.scss';
4
- import { Modal, useConfig, useModal, useTranslation } from '@payloadcms/ui';
4
+ import { Modal, useConfig, useModal, useRouteTransition, useTranslation } from '@payloadcms/ui';
5
5
  import { ArrowBigUp, ChevronLeft, Command as CommandIcon, Option } from 'lucide-react';
6
- import { useRouter } from 'next/navigation';
6
+ import { usePathname, useRouter } from 'next/navigation';
7
+ import { formatAdminURL } from 'payload/shared';
7
8
  import { createContext, Fragment, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
8
9
  import { useHotkeys } from 'react-hotkeys-hook';
9
10
  import { createDefaultGroups } from '../utils/index';
11
+ import { getCommandMenuAction } from '../utils/registry';
10
12
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from './cmdk/index';
11
13
  import { LucideIconDynamic } from './LucideIcon';
12
14
  const MODAL_SLUG = 'command-menu';
@@ -36,7 +38,74 @@ const CommandMenuComponent = ({ pluginConfig })=>{
36
38
  const commandListRef = useRef(null);
37
39
  const { closeMenu, currentPage, groups, items, setPage } = useCommandMenu();
38
40
  const router = useRouter();
41
+ const pathname = usePathname();
42
+ const { startRouteTransition } = useRouteTransition();
39
43
  const { t } = useTranslation();
44
+ const { config } = useConfig();
45
+ // Detect which collection the user is currently viewing (if any) and whether
46
+ // they are on the list page or a specific document/create page.
47
+ // Use the configured admin route instead of hardcoding '/admin' so that
48
+ // projects with a custom adminRoute still match correctly.
49
+ const { currentCollectionSlug, isDocumentContext } = useMemo(()=>{
50
+ if (!pathname) {
51
+ return {
52
+ currentCollectionSlug: null,
53
+ isDocumentContext: false
54
+ };
55
+ }
56
+ const adminRoute = config.routes?.admin ?? '/admin';
57
+ // Escape any regex special characters in the admin route path
58
+ const escapedRoute = adminRoute.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ // Group 1 = collection slug, Group 2 (optional) = document ID or 'create'
60
+ const match = pathname.match(new RegExp(`${escapedRoute}/collections/([^/]+)(?:/([^/]+))?`));
61
+ return {
62
+ currentCollectionSlug: match?.[1] ?? null,
63
+ isDocumentContext: match?.[2] != null
64
+ };
65
+ }, [
66
+ pathname,
67
+ config.routes?.admin
68
+ ]);
69
+ /** Returns true if an item/group should be visible given the current route. */ const isEntryVisible = useCallback((entry)=>{
70
+ // --- collectionSlugs filter ---
71
+ if (entry.collectionSlugs && entry.collectionSlugs.length > 0) {
72
+ if (currentCollectionSlug === null || !entry.collectionSlugs.includes(currentCollectionSlug)) {
73
+ return false;
74
+ }
75
+ }
76
+ // --- collectionContext filter ---
77
+ if (entry.collectionContext && entry.collectionContext.length > 0) {
78
+ // context filter only makes sense on a collection page
79
+ if (currentCollectionSlug === null) {
80
+ return false;
81
+ }
82
+ const currentContext = isDocumentContext ? 'document' : 'list';
83
+ if (!entry.collectionContext.includes(currentContext)) {
84
+ return false;
85
+ }
86
+ }
87
+ return true;
88
+ }, [
89
+ currentCollectionSlug,
90
+ isDocumentContext
91
+ ]);
92
+ // Filter groups to only those visible on the current page
93
+ const visibleGroups = useMemo(()=>{
94
+ return groups.filter((group)=>isEntryVisible(group)).map((group)=>({
95
+ ...group,
96
+ items: group.items.filter((item)=>isEntryVisible(item))
97
+ })).filter((group)=>group.items.length > 0);
98
+ }, [
99
+ groups,
100
+ isEntryVisible
101
+ ]);
102
+ // Filter stray items to only those visible on the current page
103
+ const visibleItems = useMemo(()=>{
104
+ return items.filter((item)=>isEntryVisible(item));
105
+ }, [
106
+ items,
107
+ isEntryVisible
108
+ ]);
40
109
  useEffect(()=>{
41
110
  setIsMac(/Mac|iPhone|iPod|iPad/i.test(navigator.platform));
42
111
  }, []);
@@ -79,6 +148,7 @@ const CommandMenuComponent = ({ pluginConfig })=>{
79
148
  children: elements
80
149
  });
81
150
  };
151
+ const adminRoute = config.routes?.admin ?? '/admin';
82
152
  // Debounced search for submenu
83
153
  useEffect(()=>{
84
154
  if (currentPage === 'main') {
@@ -99,7 +169,10 @@ const CommandMenuComponent = ({ pluginConfig })=>{
99
169
  type: 'custom',
100
170
  action: {
101
171
  type: 'link',
102
- href: `/admin/collections/${currentPage.slug}/${doc.id}`
172
+ href: formatAdminURL({
173
+ adminRoute,
174
+ path: `/collections/${currentPage.slug}/${doc.id}`
175
+ })
103
176
  },
104
177
  icon: pluginConfig?.submenu?.icons?.[currentPage.slug] ?? undefined,
105
178
  label: doc[currentPage.useAsTitle] || doc.id
@@ -117,7 +190,8 @@ const CommandMenuComponent = ({ pluginConfig })=>{
117
190
  }, [
118
191
  search,
119
192
  currentPage,
120
- pluginConfig?.submenu?.icons
193
+ pluginConfig?.submenu?.icons,
194
+ adminRoute
121
195
  ]);
122
196
  const handleBack = useCallback(()=>{
123
197
  setPage('main');
@@ -127,30 +201,53 @@ const CommandMenuComponent = ({ pluginConfig })=>{
127
201
  setPage
128
202
  ]);
129
203
  const executeItemAction = useCallback(async (item)=>{
130
- // Execute the item's action
131
- switch(item.action.type){
132
- case 'api':
133
- await fetch(item.action.href, {
134
- body: item.action.body ? JSON.stringify(item.action.body) : undefined,
135
- headers: {
136
- 'Content-Type': 'application/json'
137
- },
138
- method: item.action.method || 'GET'
139
- });
140
- break;
141
- case 'link':
142
- router.push(item.action.href);
143
- break;
144
- default:
145
- break;
204
+ try {
205
+ // Execute the item's action
206
+ switch(item.action.type){
207
+ case 'api':
208
+ {
209
+ const response = await fetch(item.action.href, {
210
+ body: item.action.body ? JSON.stringify(item.action.body) : undefined,
211
+ headers: {
212
+ 'Content-Type': 'application/json'
213
+ },
214
+ method: item.action.method || 'GET'
215
+ });
216
+ if (!response.ok) {
217
+ throw new Error(`[payload-cmdk] API action failed: ${response.status} ${response.statusText}`);
218
+ }
219
+ break;
220
+ }
221
+ case 'function':
222
+ {
223
+ const handler = getCommandMenuAction(item.action.key);
224
+ if (handler) {
225
+ await handler();
226
+ } else {
227
+ //eslint-disable-next-line no-console
228
+ console.warn(`[payload-cmdk] No handler registered for function action key "${item.action.key}". ` + `Call registerCommandMenuAction("${item.action.key}", fn) on the client.`);
229
+ }
230
+ break;
231
+ }
232
+ case 'link':
233
+ {
234
+ const href = item.action.href;
235
+ startRouteTransition(()=>router.push(href));
236
+ break;
237
+ }
238
+ default:
239
+ break;
240
+ }
241
+ } finally{
242
+ closeMenu();
243
+ setSearch('');
244
+ setPage('main');
146
245
  }
147
- closeMenu();
148
- setSearch('');
149
- setPage('main');
150
246
  }, [
151
247
  router,
152
248
  closeMenu,
153
- setPage
249
+ setPage,
250
+ startRouteTransition
154
251
  ]);
155
252
  const openSubmenu = useCallback((item)=>{
156
253
  setPage({
@@ -194,7 +291,7 @@ const CommandMenuComponent = ({ pluginConfig })=>{
194
291
  e.preventDefault();
195
292
  e.stopPropagation();
196
293
  // Find the item in groups
197
- const item = groups.flatMap((g)=>g.items).find((i)=>i.slug === itemSlug);
294
+ const item = visibleGroups.flatMap((g)=>g.items).find((i)=>i.slug === itemSlug);
198
295
  if (item) {
199
296
  openSubmenu(item);
200
297
  }
@@ -211,7 +308,7 @@ const CommandMenuComponent = ({ pluginConfig })=>{
211
308
  submenuEnabled,
212
309
  submenuShortcut,
213
310
  openSubmenu,
214
- groups
311
+ visibleGroups
215
312
  ]);
216
313
  const placeholder = currentPage === 'main' ? t('cmdkPlugin:search') : t('general:searchBy', {
217
314
  label: currentPage.useAsTitleLabel
@@ -293,7 +390,7 @@ const CommandMenuComponent = ({ pluginConfig })=>{
293
390
  /*#__PURE__*/ _jsx(CommandEmpty, {
294
391
  children: isLoadingSubmenu ? t('cmdkPlugin:loading') : t('cmdkPlugin:noResults')
295
392
  }),
296
- currentPage === 'main' && groups.map((group, index)=>{
393
+ currentPage === 'main' && visibleGroups.map((group, index)=>{
297
394
  if (group.items.length === 0) {
298
395
  return null;
299
396
  }
@@ -304,7 +401,7 @@ const CommandMenuComponent = ({ pluginConfig })=>{
304
401
  if (group.title === 'Globals') {
305
402
  titleName = t('general:globals');
306
403
  }
307
- const isRenderSeparator = !(index === groups.length - 1 && items.length === 0);
404
+ const isRenderSeparator = !(index === visibleGroups.length - 1 && visibleItems.length === 0);
308
405
  return /*#__PURE__*/ _jsxs(Fragment, {
309
406
  children: [
310
407
  /*#__PURE__*/ _jsx(CommandGroup, {
@@ -340,7 +437,7 @@ const CommandMenuComponent = ({ pluginConfig })=>{
340
437
  ]
341
438
  }, group.title);
342
439
  }),
343
- currentPage === 'main' && items?.map((item)=>{
440
+ currentPage === 'main' && visibleItems?.map((item)=>{
344
441
  const isDynamicIcon = typeof item.icon === 'string';
345
442
  const IconComponent = isDynamicIcon ? null : item.icon;
346
443
  return /*#__PURE__*/ _jsxs(CommandItem, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/CommandMenuContext.tsx"],"sourcesContent":["'use client'\nimport type { LucideIcon } from 'lucide-react'\nimport type { CustomTranslationsKeys, CustomTranslationsObject } from 'src/translations/index'\n\nimport './modal.scss'\n\nimport type {\n CommandMenuContextProps,\n CommandMenuGroup,\n CommandMenuItem,\n CommandMenuPage,\n GenericCollectionDocument,\n IconName,\n PluginCommandMenuConfig,\n} from 'src/types'\n\nimport { Modal, useConfig, useModal, useTranslation } from '@payloadcms/ui'\nimport { ArrowBigUp, ChevronLeft, Command as CommandIcon, Option } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport {\n createContext,\n Fragment,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport { createDefaultGroups } from '../utils/index'\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut,\n} from './cmdk/index'\nimport { LucideIconDynamic } from './LucideIcon'\n\nconst MODAL_SLUG = 'command-menu'\n\ninterface CommandMenuContextType {\n closeMenu: () => void\n currentPage: CommandMenuPage\n groups: CommandMenuGroup[]\n isOpen: boolean\n items: CommandMenuItem[]\n openMenu: () => void\n setPage: (page: CommandMenuPage) => void\n toggleMenu: () => void\n}\n\nconst CommandMenuContext = createContext<CommandMenuContextType>({\n closeMenu: () => {},\n currentPage: 'main',\n groups: [],\n isOpen: false,\n items: [],\n openMenu: () => {},\n setPage: () => {},\n toggleMenu: () => {},\n})\n\nexport const useCommandMenu = () => {\n const context = useContext(CommandMenuContext)\n return context\n}\n\nconst ITEM_SELECTOR = `[cmdk-item=\"\"]`\n\nfunction getSelectedElement(containerRef: React.RefObject<HTMLElement | null>) {\n return containerRef.current?.querySelector(`${ITEM_SELECTOR}[aria-selected=\"true\"]`)\n}\n\nconst CommandMenuComponent: React.FC<{\n pluginConfig: PluginCommandMenuConfig\n}> = ({ pluginConfig }) => {\n const [search, setSearch] = useState('')\n const [submenuItems, setSubmenuItems] = useState<CommandMenuItem[]>([])\n const [isLoadingSubmenu, setIsLoadingSubmenu] = useState(false)\n const [isMac, setIsMac] = useState(false)\n\n const commandListRef = useRef<HTMLDivElement>(null)\n\n const { closeMenu, currentPage, groups, items, setPage } = useCommandMenu()\n const router = useRouter()\n const { t } = useTranslation<CustomTranslationsObject, CustomTranslationsKeys>()\n\n useEffect(() => {\n setIsMac(/Mac|iPhone|iPod|iPad/i.test(navigator.platform))\n }, [])\n\n const submenuEnabled = pluginConfig?.submenu?.enabled !== false\n const submenuShortcut = pluginConfig?.submenu?.shortcut || 'shift+enter'\n const blurBg = pluginConfig?.blurBg !== false\n\n const formatShortcutKey = (key: string): React.ReactNode => {\n // Handle compound shortcuts like \"Shift + Enter\"\n const parts = key.split('+').map((part) => part.trim())\n const elements = parts.map((part, index) => {\n const lowerPart = part.toLowerCase()\n let content: React.ReactNode\n\n if (lowerPart === 'ctrl' || lowerPart === 'cmd') {\n content = isMac ? <CommandIcon size={12} /> : 'Ctrl'\n } else if (lowerPart === 'meta') {\n content = isMac ? <CommandIcon size={12} /> : 'Ctrl'\n } else if (lowerPart === 'shift') {\n content = isMac ? <ArrowBigUp size={12} /> : 'Shift'\n } else if (lowerPart === 'alt') {\n content = isMac ? <Option size={12} /> : 'Alt'\n } else {\n content = part\n }\n\n return (\n <Fragment key={index}>\n {content}\n {!isMac && index < parts.length - 1 && ' + '}\n </Fragment>\n )\n })\n\n return <>{elements}</>\n }\n\n // Debounced search for submenu\n useEffect(() => {\n if (currentPage === 'main') {\n return\n }\n\n const fetchDocuments = async () => {\n if (currentPage.type !== 'collection-search') {\n return\n }\n\n setIsLoadingSubmenu(true)\n try {\n const searchParam = search\n ? `&where[${currentPage.useAsTitle}][like]=${encodeURIComponent(search)}`\n : ''\n const response = await fetch(\n `/api/${currentPage.slug}?limit=10${searchParam}&select[${currentPage.useAsTitle}]=true`,\n )\n const data = await response.json()\n\n if (data.docs && Array.isArray(data.docs)) {\n const docs: CommandMenuItem[] = data.docs.map((doc: GenericCollectionDocument) => ({\n slug: `${currentPage.slug}-${doc.id}`,\n type: 'custom' as const,\n action: {\n type: 'link',\n href: `/admin/collections/${currentPage.slug}/${doc.id}`,\n },\n icon: pluginConfig?.submenu?.icons?.[currentPage.slug] ?? undefined,\n label: doc[currentPage.useAsTitle] || doc.id,\n }))\n setSubmenuItems(docs)\n }\n } catch {\n setSubmenuItems([])\n } finally {\n setIsLoadingSubmenu(false)\n }\n }\n\n const timeoutId = setTimeout(fetchDocuments, 300)\n return () => clearTimeout(timeoutId)\n }, [search, currentPage, pluginConfig?.submenu?.icons])\n\n const handleBack = useCallback(() => {\n setPage('main')\n setSearch('')\n setSubmenuItems([])\n }, [setPage])\n\n const executeItemAction = useCallback(\n async (item: CommandMenuItem) => {\n // Execute the item's action\n switch (item.action.type) {\n case 'api':\n await fetch(item.action.href, {\n body: item.action.body ? JSON.stringify(item.action.body) : undefined,\n headers: {\n 'Content-Type': 'application/json',\n },\n method: item.action.method || 'GET',\n })\n break\n case 'link':\n router.push(item.action.href)\n break\n default:\n break\n }\n closeMenu()\n setSearch('')\n setPage('main')\n },\n [router, closeMenu, setPage],\n )\n\n const openSubmenu = useCallback(\n (item: CommandMenuItem) => {\n setPage({\n slug: item.slug,\n type: 'collection-search',\n label: item.label,\n useAsTitle: item.useAsTitle || 'id',\n useAsTitleLabel: item.useAsTitleLabel || item.useAsTitle || 'id',\n })\n setSearch('')\n //set isLoadingSubmenu to true to show loading state while fetching\n setIsLoadingSubmenu(true)\n setSubmenuItems([])\n },\n [setPage],\n )\n\n const handleSelect = useCallback(\n async (item: CommandMenuItem) => {\n await executeItemAction(item)\n },\n [executeItemAction],\n )\n\n // Handle keyboard events for navigation and back\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n // ESC key for back navigation in submenu\n if (e.key === 'Escape' && currentPage !== 'main') {\n e.preventDefault()\n e.stopPropagation()\n handleBack()\n return\n }\n\n // Enter/Shift+Enter handling for collection submenu\n if (e.key === 'Enter' && currentPage === 'main') {\n const selectedElement = getSelectedElement(commandListRef)\n const itemType = selectedElement?.getAttribute('data-item-type')\n const itemSlug = selectedElement?.getAttribute('data-value')\n\n if (submenuEnabled && itemType === 'collection' && itemSlug) {\n const isShiftPressed = e.shiftKey\n const shouldOpenSubmenu =\n (submenuShortcut === 'shift+enter' && isShiftPressed) ||\n (submenuShortcut === 'enter' && !isShiftPressed)\n\n if (shouldOpenSubmenu) {\n e.preventDefault()\n e.stopPropagation()\n\n // Find the item in groups\n const item = groups.flatMap((g) => g.items).find((i) => i.slug === itemSlug)\n if (item) {\n openSubmenu(item)\n }\n return\n }\n }\n }\n }\n\n document.addEventListener('keydown', handleKeyDown, true)\n return () => document.removeEventListener('keydown', handleKeyDown, true)\n }, [currentPage, handleBack, submenuEnabled, submenuShortcut, openSubmenu, groups])\n\n const placeholder =\n currentPage === 'main'\n ? t('cmdkPlugin:search')\n : t('general:searchBy', {\n label: currentPage.useAsTitleLabel,\n })\n\n const shouldDisableFilter = currentPage !== 'main'\n\n // Static footer shortcuts based on current page\n const footerShortcuts =\n currentPage === 'main' && submenuEnabled && submenuShortcut === 'shift+enter'\n ? [\n { key: 'Enter', label: t('cmdkPlugin:navigate') },\n { key: 'Shift + Enter', label: t('cmdkPlugin:searchInCollection') },\n ]\n : currentPage === 'main' && submenuEnabled && submenuShortcut === 'enter'\n ? [\n { key: 'Enter', label: t('cmdkPlugin:searchInCollection') },\n { key: 'Shift + Enter', label: t('cmdkPlugin:navigate') },\n ]\n : currentPage === 'main'\n ? [{ key: 'Enter', label: t('cmdkPlugin:navigate') }]\n : [{ key: 'Enter', label: t('cmdkPlugin:open') }]\n\n const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {\n // Close modal only if clicking the backdrop (not the command itself)\n if (e.target === e.currentTarget) {\n closeMenu()\n }\n }\n // console.log('Rendering CommandMenuComponent', { currentPage, groups, items, submenuItems })\n return (\n <Modal slug={MODAL_SLUG}>\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}\n <div\n className={`command-modal ${blurBg ? 'command-modal--blur' : ''}`}\n onClick={handleBackdropClick}\n >\n <Command label=\"Command Menu\" shouldFilter={!shouldDisableFilter}>\n {/* Header for submenu navigation */}\n {currentPage !== 'main' && (\n <div className=\"command__header\">\n <button className=\"command__back-button\" onClick={handleBack} type=\"button\">\n <ChevronLeft size={16} />\n </button>\n <span className=\"command__header-label\">\n {t('cmdkPlugin:searchIn', { label: currentPage.label })}\n </span>\n </div>\n )}\n\n <CommandInput onValueChange={setSearch} placeholder={placeholder} value={search} />\n <CommandList ref={commandListRef}>\n <CommandEmpty>\n {isLoadingSubmenu ? t('cmdkPlugin:loading') : t('cmdkPlugin:noResults')}\n </CommandEmpty>\n\n {/* Main page view */}\n {currentPage === 'main' &&\n groups.map((group, index) => {\n if (group.items.length === 0) {\n return null\n }\n\n let titleName = group.title\n if (group.title === 'Collections') {\n titleName = t('general:collections')\n }\n if (group.title === 'Globals') {\n titleName = t('general:globals')\n }\n\n const isRenderSeparator = !(index === groups.length - 1 && items.length === 0)\n return (\n <Fragment key={group.title}>\n <CommandGroup heading={titleName}>\n {group.items.map((item) => {\n const isDynamicIcon = typeof item.icon === 'string'\n const IconComponent = isDynamicIcon ? null : (item.icon as LucideIcon)\n return (\n <CommandItem\n data-action-type={item.action.type}\n data-item-type={item.type}\n key={item.slug}\n keywords={[titleName, item.label]}\n onSelect={() => handleSelect(item)}\n value={item.slug}\n >\n {isDynamicIcon ? (\n <LucideIconDynamic\n className=\"command__item-icon\"\n name={item.icon as IconName}\n />\n ) : (\n IconComponent && <IconComponent className=\"command__item-icon\" />\n )}\n {item.label}\n {submenuEnabled && item.type === 'collection' && (\n <CommandShortcut>›</CommandShortcut>\n )}\n </CommandItem>\n )\n })}\n </CommandGroup>\n {isRenderSeparator && <CommandSeparator />}\n </Fragment>\n )\n })}\n\n {/* Stray items on main page */}\n {currentPage === 'main' &&\n items?.map((item) => {\n const isDynamicIcon = typeof item.icon === 'string'\n const IconComponent = isDynamicIcon ? null : (item.icon as LucideIcon)\n return (\n <CommandItem\n data-action-type={item.action.type}\n data-item-type={item.type}\n key={item.slug}\n keywords={[item.label]}\n onSelect={() => handleSelect(item)}\n value={item.slug}\n >\n {isDynamicIcon ? (\n <LucideIconDynamic\n className=\"command__item-icon\"\n name={item.icon as IconName}\n />\n ) : (\n IconComponent && <IconComponent className=\"command__item-icon\" />\n )}\n {item.label}\n </CommandItem>\n )\n })}\n\n {/* Submenu page view */}\n {currentPage !== 'main' &&\n submenuItems.map((item) => {\n const isDynamicIcon = typeof item.icon === 'string'\n const IconComponent = isDynamicIcon ? null : (item.icon as LucideIcon)\n return (\n <CommandItem\n data-action-type={item.action.type}\n data-item-type={item.type}\n key={item.slug}\n onSelect={() => handleSelect(item)}\n value={item.slug}\n >\n {isDynamicIcon ? (\n <LucideIconDynamic\n className=\"command__item-icon\"\n name={item.icon as IconName}\n />\n ) : (\n IconComponent && <IconComponent className=\"command__item-icon\" />\n )}\n {item.label}\n </CommandItem>\n )\n })}\n </CommandList>\n\n {/* Footer with static shortcuts */}\n {footerShortcuts && footerShortcuts.length > 0 && (\n <div className=\"command__footer\">\n {footerShortcuts.map((shortcut, index) => (\n <span key={index}>\n <kbd>{formatShortcutKey(shortcut.key)}</kbd> {shortcut.label}\n </span>\n ))}\n </div>\n )}\n </Command>\n </div>\n </Modal>\n )\n}\n\nconst MemoizedCommandMenuComponent = memo(CommandMenuComponent)\n\nexport const CommandMenuProvider: React.FC<CommandMenuContextProps> = ({\n children,\n pluginConfig,\n}) => {\n const { closeModal, isModalOpen, openModal, toggleModal } = useModal()\n const [currentPage, setCurrentPage] = useState<CommandMenuPage>('main')\n\n useHotkeys(\n pluginConfig.shortcut || ['meta+k', 'ctrl+k'],\n (event) => {\n event.preventDefault()\n event.stopPropagation()\n toggleModal(MODAL_SLUG)\n },\n [toggleModal],\n )\n const { config } = useConfig()\n const { i18n } = useTranslation()\n const currentLang = i18n.language\n const { groups, items } = useMemo(() => {\n return createDefaultGroups(config, currentLang, pluginConfig)\n }, [config, currentLang, pluginConfig])\n\n return (\n <CommandMenuContext.Provider\n value={{\n closeMenu: () => closeModal(MODAL_SLUG),\n currentPage,\n groups,\n isOpen: isModalOpen(MODAL_SLUG),\n items,\n openMenu: () => openModal(MODAL_SLUG),\n setPage: setCurrentPage,\n toggleMenu: () => toggleModal(MODAL_SLUG),\n }}\n >\n {children}\n <MemoizedCommandMenuComponent pluginConfig={pluginConfig} />\n </CommandMenuContext.Provider>\n )\n}\n"],"names":["Modal","useConfig","useModal","useTranslation","ArrowBigUp","ChevronLeft","Command","CommandIcon","Option","useRouter","createContext","Fragment","memo","useCallback","useContext","useEffect","useMemo","useRef","useState","useHotkeys","createDefaultGroups","CommandEmpty","CommandGroup","CommandInput","CommandItem","CommandList","CommandSeparator","CommandShortcut","LucideIconDynamic","MODAL_SLUG","CommandMenuContext","closeMenu","currentPage","groups","isOpen","items","openMenu","setPage","toggleMenu","useCommandMenu","context","ITEM_SELECTOR","getSelectedElement","containerRef","current","querySelector","CommandMenuComponent","pluginConfig","search","setSearch","submenuItems","setSubmenuItems","isLoadingSubmenu","setIsLoadingSubmenu","isMac","setIsMac","commandListRef","router","t","test","navigator","platform","submenuEnabled","submenu","enabled","submenuShortcut","shortcut","blurBg","formatShortcutKey","key","parts","split","map","part","trim","elements","index","lowerPart","toLowerCase","content","size","length","fetchDocuments","type","searchParam","useAsTitle","encodeURIComponent","response","fetch","slug","data","json","docs","Array","isArray","doc","id","action","href","icon","icons","undefined","label","timeoutId","setTimeout","clearTimeout","handleBack","executeItemAction","item","body","JSON","stringify","headers","method","push","openSubmenu","useAsTitleLabel","handleSelect","handleKeyDown","e","preventDefault","stopPropagation","selectedElement","itemType","getAttribute","itemSlug","isShiftPressed","shiftKey","shouldOpenSubmenu","flatMap","g","find","i","document","addEventListener","removeEventListener","placeholder","shouldDisableFilter","footerShortcuts","handleBackdropClick","target","currentTarget","div","className","onClick","shouldFilter","button","span","onValueChange","value","ref","group","titleName","title","isRenderSeparator","heading","isDynamicIcon","IconComponent","data-action-type","data-item-type","keywords","onSelect","name","kbd","MemoizedCommandMenuComponent","CommandMenuProvider","children","closeModal","isModalOpen","openModal","toggleModal","setCurrentPage","event","config","i18n","currentLang","language","Provider"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/components/CommandMenuContext.tsx"],"sourcesContent":["'use client'\nimport type { LucideIcon } from 'lucide-react'\nimport type { CustomTranslationsKeys, CustomTranslationsObject } from 'src/translations/index'\n\nimport './modal.scss'\n\nimport type {\n CollectionContext,\n CommandMenuContextProps,\n CommandMenuGroup,\n CommandMenuItem,\n CommandMenuPage,\n GenericCollectionDocument,\n IconName,\n PluginCommandMenuConfig,\n} from 'src/types'\n\nimport { Modal, useConfig, useModal, useRouteTransition, useTranslation } from '@payloadcms/ui'\nimport { ArrowBigUp, ChevronLeft, Command as CommandIcon, Option } from 'lucide-react'\nimport { usePathname, useRouter } from 'next/navigation'\nimport { formatAdminURL } from 'payload/shared'\nimport {\n createContext,\n Fragment,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport { createDefaultGroups } from '../utils/index'\nimport { getCommandMenuAction } from '../utils/registry'\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut,\n} from './cmdk/index'\nimport { LucideIconDynamic } from './LucideIcon'\n\nconst MODAL_SLUG = 'command-menu'\n\ninterface CommandMenuContextType {\n closeMenu: () => void\n currentPage: CommandMenuPage\n groups: CommandMenuGroup[]\n isOpen: boolean\n items: CommandMenuItem[]\n openMenu: () => void\n setPage: (page: CommandMenuPage) => void\n toggleMenu: () => void\n}\n\nconst CommandMenuContext = createContext<CommandMenuContextType>({\n closeMenu: () => {},\n currentPage: 'main',\n groups: [],\n isOpen: false,\n items: [],\n openMenu: () => {},\n setPage: () => {},\n toggleMenu: () => {},\n})\n\nexport const useCommandMenu = () => {\n const context = useContext(CommandMenuContext)\n return context\n}\n\nconst ITEM_SELECTOR = `[cmdk-item=\"\"]`\n\nfunction getSelectedElement(containerRef: React.RefObject<HTMLElement | null>) {\n return containerRef.current?.querySelector(`${ITEM_SELECTOR}[aria-selected=\"true\"]`)\n}\n\nconst CommandMenuComponent: React.FC<{\n pluginConfig: PluginCommandMenuConfig\n}> = ({ pluginConfig }) => {\n const [search, setSearch] = useState('')\n const [submenuItems, setSubmenuItems] = useState<CommandMenuItem[]>([])\n const [isLoadingSubmenu, setIsLoadingSubmenu] = useState(false)\n const [isMac, setIsMac] = useState(false)\n\n const commandListRef = useRef<HTMLDivElement>(null)\n\n const { closeMenu, currentPage, groups, items, setPage } = useCommandMenu()\n const router = useRouter()\n const pathname = usePathname()\n const { startRouteTransition } = useRouteTransition()\n const { t } = useTranslation<CustomTranslationsObject, CustomTranslationsKeys>()\n const { config } = useConfig()\n\n // Detect which collection the user is currently viewing (if any) and whether\n // they are on the list page or a specific document/create page.\n // Use the configured admin route instead of hardcoding '/admin' so that\n // projects with a custom adminRoute still match correctly.\n const { currentCollectionSlug, isDocumentContext } = useMemo(() => {\n if (!pathname) {\n return { currentCollectionSlug: null, isDocumentContext: false }\n }\n const adminRoute = config.routes?.admin ?? '/admin'\n // Escape any regex special characters in the admin route path\n const escapedRoute = adminRoute.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n // Group 1 = collection slug, Group 2 (optional) = document ID or 'create'\n const match = pathname.match(new RegExp(`${escapedRoute}/collections/([^/]+)(?:/([^/]+))?`))\n return {\n currentCollectionSlug: match?.[1] ?? null,\n isDocumentContext: match?.[2] != null,\n }\n }, [pathname, config.routes?.admin])\n\n /** Returns true if an item/group should be visible given the current route. */\n const isEntryVisible = useCallback(\n (entry: {\n collectionContext?: CollectionContext\n collectionSlugs?: readonly string[] | string[]\n }): boolean => {\n // --- collectionSlugs filter ---\n if (entry.collectionSlugs && entry.collectionSlugs.length > 0) {\n if (\n currentCollectionSlug === null ||\n !(entry.collectionSlugs as string[]).includes(currentCollectionSlug)\n ) {\n return false\n }\n }\n // --- collectionContext filter ---\n if (entry.collectionContext && entry.collectionContext.length > 0) {\n // context filter only makes sense on a collection page\n if (currentCollectionSlug === null) {\n return false\n }\n const currentContext = isDocumentContext ? 'document' : 'list'\n if (!entry.collectionContext.includes(currentContext)) {\n return false\n }\n }\n return true\n },\n [currentCollectionSlug, isDocumentContext],\n )\n\n // Filter groups to only those visible on the current page\n const visibleGroups = useMemo(() => {\n return groups\n .filter((group) => isEntryVisible(group))\n .map((group) => ({\n ...group,\n items: group.items.filter((item) => isEntryVisible(item)),\n }))\n .filter((group) => group.items.length > 0)\n }, [groups, isEntryVisible])\n\n // Filter stray items to only those visible on the current page\n const visibleItems = useMemo(() => {\n return items.filter((item) => isEntryVisible(item))\n }, [items, isEntryVisible])\n\n useEffect(() => {\n setIsMac(/Mac|iPhone|iPod|iPad/i.test(navigator.platform))\n }, [])\n\n const submenuEnabled = pluginConfig?.submenu?.enabled !== false\n const submenuShortcut = pluginConfig?.submenu?.shortcut || 'shift+enter'\n const blurBg = pluginConfig?.blurBg !== false\n\n const formatShortcutKey = (key: string): React.ReactNode => {\n // Handle compound shortcuts like \"Shift + Enter\"\n const parts = key.split('+').map((part) => part.trim())\n const elements = parts.map((part, index) => {\n const lowerPart = part.toLowerCase()\n let content: React.ReactNode\n\n if (lowerPart === 'ctrl' || lowerPart === 'cmd') {\n content = isMac ? <CommandIcon size={12} /> : 'Ctrl'\n } else if (lowerPart === 'meta') {\n content = isMac ? <CommandIcon size={12} /> : 'Ctrl'\n } else if (lowerPart === 'shift') {\n content = isMac ? <ArrowBigUp size={12} /> : 'Shift'\n } else if (lowerPart === 'alt') {\n content = isMac ? <Option size={12} /> : 'Alt'\n } else {\n content = part\n }\n\n return (\n <Fragment key={index}>\n {content}\n {!isMac && index < parts.length - 1 && ' + '}\n </Fragment>\n )\n })\n\n return <>{elements}</>\n }\n\n const adminRoute = config.routes?.admin ?? '/admin'\n // Debounced search for submenu\n useEffect(() => {\n if (currentPage === 'main') {\n return\n }\n\n const fetchDocuments = async () => {\n if (currentPage.type !== 'collection-search') {\n return\n }\n\n setIsLoadingSubmenu(true)\n try {\n const searchParam = search\n ? `&where[${currentPage.useAsTitle}][like]=${encodeURIComponent(search)}`\n : ''\n const response = await fetch(\n `/api/${currentPage.slug}?limit=10${searchParam}&select[${currentPage.useAsTitle}]=true`,\n )\n const data = await response.json()\n\n if (data.docs && Array.isArray(data.docs)) {\n const docs: CommandMenuItem[] = data.docs.map((doc: GenericCollectionDocument) => ({\n slug: `${currentPage.slug}-${doc.id}`,\n type: 'custom' as const,\n action: {\n type: 'link',\n href: formatAdminURL({\n adminRoute,\n path: `/collections/${currentPage.slug}/${doc.id}`,\n }),\n },\n icon: pluginConfig?.submenu?.icons?.[currentPage.slug] ?? undefined,\n label: doc[currentPage.useAsTitle] || doc.id,\n }))\n setSubmenuItems(docs)\n }\n } catch {\n setSubmenuItems([])\n } finally {\n setIsLoadingSubmenu(false)\n }\n }\n\n const timeoutId = setTimeout(fetchDocuments, 300)\n return () => clearTimeout(timeoutId)\n }, [search, currentPage, pluginConfig?.submenu?.icons, adminRoute])\n\n const handleBack = useCallback(() => {\n setPage('main')\n setSearch('')\n setSubmenuItems([])\n }, [setPage])\n\n const executeItemAction = useCallback(\n async (item: CommandMenuItem) => {\n try {\n // Execute the item's action\n switch (item.action.type) {\n case 'api': {\n const response = await fetch(item.action.href, {\n body: item.action.body ? JSON.stringify(item.action.body) : undefined,\n headers: {\n 'Content-Type': 'application/json',\n },\n method: item.action.method || 'GET',\n })\n if (!response.ok) {\n throw new Error(\n `[payload-cmdk] API action failed: ${response.status} ${response.statusText}`,\n )\n }\n break\n }\n case 'function': {\n const handler = getCommandMenuAction(item.action.key)\n if (handler) {\n await handler()\n } else {\n //eslint-disable-next-line no-console\n console.warn(\n `[payload-cmdk] No handler registered for function action key \"${item.action.key}\". ` +\n `Call registerCommandMenuAction(\"${item.action.key}\", fn) on the client.`,\n )\n }\n break\n }\n case 'link': {\n const href = item.action.href\n startRouteTransition(() => router.push(href))\n break\n }\n default:\n break\n }\n } finally {\n closeMenu()\n setSearch('')\n setPage('main')\n }\n },\n [router, closeMenu, setPage, startRouteTransition],\n )\n\n const openSubmenu = useCallback(\n (item: CommandMenuItem) => {\n setPage({\n slug: item.slug,\n type: 'collection-search',\n label: item.label,\n useAsTitle: item.useAsTitle || 'id',\n useAsTitleLabel: item.useAsTitleLabel || item.useAsTitle || 'id',\n })\n setSearch('')\n //set isLoadingSubmenu to true to show loading state while fetching\n setIsLoadingSubmenu(true)\n setSubmenuItems([])\n },\n [setPage],\n )\n\n const handleSelect = useCallback(\n async (item: CommandMenuItem) => {\n await executeItemAction(item)\n },\n [executeItemAction],\n )\n\n // Handle keyboard events for navigation and back\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n // ESC key for back navigation in submenu\n if (e.key === 'Escape' && currentPage !== 'main') {\n e.preventDefault()\n e.stopPropagation()\n handleBack()\n return\n }\n\n // Enter/Shift+Enter handling for collection submenu\n if (e.key === 'Enter' && currentPage === 'main') {\n const selectedElement = getSelectedElement(commandListRef)\n const itemType = selectedElement?.getAttribute('data-item-type')\n const itemSlug = selectedElement?.getAttribute('data-value')\n\n if (submenuEnabled && itemType === 'collection' && itemSlug) {\n const isShiftPressed = e.shiftKey\n const shouldOpenSubmenu =\n (submenuShortcut === 'shift+enter' && isShiftPressed) ||\n (submenuShortcut === 'enter' && !isShiftPressed)\n\n if (shouldOpenSubmenu) {\n e.preventDefault()\n e.stopPropagation()\n\n // Find the item in groups\n const item = visibleGroups.flatMap((g) => g.items).find((i) => i.slug === itemSlug)\n if (item) {\n openSubmenu(item)\n }\n return\n }\n }\n }\n }\n\n document.addEventListener('keydown', handleKeyDown, true)\n return () => document.removeEventListener('keydown', handleKeyDown, true)\n }, [currentPage, handleBack, submenuEnabled, submenuShortcut, openSubmenu, visibleGroups])\n\n const placeholder =\n currentPage === 'main'\n ? t('cmdkPlugin:search')\n : t('general:searchBy', {\n label: currentPage.useAsTitleLabel,\n })\n\n const shouldDisableFilter = currentPage !== 'main'\n\n // Static footer shortcuts based on current page\n const footerShortcuts =\n currentPage === 'main' && submenuEnabled && submenuShortcut === 'shift+enter'\n ? [\n { key: 'Enter', label: t('cmdkPlugin:navigate') },\n { key: 'Shift + Enter', label: t('cmdkPlugin:searchInCollection') },\n ]\n : currentPage === 'main' && submenuEnabled && submenuShortcut === 'enter'\n ? [\n { key: 'Enter', label: t('cmdkPlugin:searchInCollection') },\n { key: 'Shift + Enter', label: t('cmdkPlugin:navigate') },\n ]\n : currentPage === 'main'\n ? [{ key: 'Enter', label: t('cmdkPlugin:navigate') }]\n : [{ key: 'Enter', label: t('cmdkPlugin:open') }]\n\n const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {\n // Close modal only if clicking the backdrop (not the command itself)\n if (e.target === e.currentTarget) {\n closeMenu()\n }\n }\n // console.log('Rendering CommandMenuComponent', { currentPage, groups, items, submenuItems })\n return (\n <Modal slug={MODAL_SLUG}>\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}\n <div\n className={`command-modal ${blurBg ? 'command-modal--blur' : ''}`}\n onClick={handleBackdropClick}\n >\n <Command label=\"Command Menu\" shouldFilter={!shouldDisableFilter}>\n {/* Header for submenu navigation */}\n {currentPage !== 'main' && (\n <div className=\"command__header\">\n <button className=\"command__back-button\" onClick={handleBack} type=\"button\">\n <ChevronLeft size={16} />\n </button>\n <span className=\"command__header-label\">\n {t('cmdkPlugin:searchIn', { label: currentPage.label })}\n </span>\n </div>\n )}\n\n <CommandInput onValueChange={setSearch} placeholder={placeholder} value={search} />\n <CommandList ref={commandListRef}>\n <CommandEmpty>\n {isLoadingSubmenu ? t('cmdkPlugin:loading') : t('cmdkPlugin:noResults')}\n </CommandEmpty>\n\n {/* Main page view */}\n {currentPage === 'main' &&\n visibleGroups.map((group, index) => {\n if (group.items.length === 0) {\n return null\n }\n\n let titleName = group.title\n if (group.title === 'Collections') {\n titleName = t('general:collections')\n }\n if (group.title === 'Globals') {\n titleName = t('general:globals')\n }\n\n const isRenderSeparator = !(\n index === visibleGroups.length - 1 && visibleItems.length === 0\n )\n return (\n <Fragment key={group.title}>\n <CommandGroup heading={titleName}>\n {group.items.map((item) => {\n const isDynamicIcon = typeof item.icon === 'string'\n const IconComponent = isDynamicIcon ? null : (item.icon as LucideIcon)\n return (\n <CommandItem\n data-action-type={item.action.type}\n data-item-type={item.type}\n key={item.slug}\n keywords={[titleName, item.label]}\n onSelect={() => handleSelect(item)}\n value={item.slug}\n >\n {isDynamicIcon ? (\n <LucideIconDynamic\n className=\"command__item-icon\"\n name={item.icon as IconName}\n />\n ) : (\n IconComponent && <IconComponent className=\"command__item-icon\" />\n )}\n {item.label}\n {submenuEnabled && item.type === 'collection' && (\n <CommandShortcut>›</CommandShortcut>\n )}\n </CommandItem>\n )\n })}\n </CommandGroup>\n {isRenderSeparator && <CommandSeparator />}\n </Fragment>\n )\n })}\n\n {/* Stray items on main page */}\n {currentPage === 'main' &&\n visibleItems?.map((item) => {\n const isDynamicIcon = typeof item.icon === 'string'\n const IconComponent = isDynamicIcon ? null : (item.icon as LucideIcon)\n return (\n <CommandItem\n data-action-type={item.action.type}\n data-item-type={item.type}\n key={item.slug}\n keywords={[item.label]}\n onSelect={() => handleSelect(item)}\n value={item.slug}\n >\n {isDynamicIcon ? (\n <LucideIconDynamic\n className=\"command__item-icon\"\n name={item.icon as IconName}\n />\n ) : (\n IconComponent && <IconComponent className=\"command__item-icon\" />\n )}\n {item.label}\n </CommandItem>\n )\n })}\n\n {/* Submenu page view */}\n {currentPage !== 'main' &&\n submenuItems.map((item) => {\n const isDynamicIcon = typeof item.icon === 'string'\n const IconComponent = isDynamicIcon ? null : (item.icon as LucideIcon)\n return (\n <CommandItem\n data-action-type={item.action.type}\n data-item-type={item.type}\n key={item.slug}\n onSelect={() => handleSelect(item)}\n value={item.slug}\n >\n {isDynamicIcon ? (\n <LucideIconDynamic\n className=\"command__item-icon\"\n name={item.icon as IconName}\n />\n ) : (\n IconComponent && <IconComponent className=\"command__item-icon\" />\n )}\n {item.label}\n </CommandItem>\n )\n })}\n </CommandList>\n\n {/* Footer with static shortcuts */}\n {footerShortcuts && footerShortcuts.length > 0 && (\n <div className=\"command__footer\">\n {footerShortcuts.map((shortcut, index) => (\n <span key={index}>\n <kbd>{formatShortcutKey(shortcut.key)}</kbd> {shortcut.label}\n </span>\n ))}\n </div>\n )}\n </Command>\n </div>\n </Modal>\n )\n}\n\nconst MemoizedCommandMenuComponent = memo(CommandMenuComponent)\n\nexport const CommandMenuProvider: React.FC<CommandMenuContextProps> = ({\n children,\n pluginConfig,\n}) => {\n const { closeModal, isModalOpen, openModal, toggleModal } = useModal()\n const [currentPage, setCurrentPage] = useState<CommandMenuPage>('main')\n\n useHotkeys(\n pluginConfig.shortcut || ['meta+k', 'ctrl+k'],\n (event) => {\n event.preventDefault()\n event.stopPropagation()\n toggleModal(MODAL_SLUG)\n },\n [toggleModal],\n )\n const { config } = useConfig()\n const { i18n } = useTranslation()\n const currentLang = i18n.language\n const { groups, items } = useMemo(() => {\n return createDefaultGroups(config, currentLang, pluginConfig)\n }, [config, currentLang, pluginConfig])\n\n return (\n <CommandMenuContext.Provider\n value={{\n closeMenu: () => closeModal(MODAL_SLUG),\n currentPage,\n groups,\n isOpen: isModalOpen(MODAL_SLUG),\n items,\n openMenu: () => openModal(MODAL_SLUG),\n setPage: setCurrentPage,\n toggleMenu: () => toggleModal(MODAL_SLUG),\n }}\n >\n {children}\n <MemoizedCommandMenuComponent pluginConfig={pluginConfig} />\n </CommandMenuContext.Provider>\n )\n}\n"],"names":["Modal","useConfig","useModal","useRouteTransition","useTranslation","ArrowBigUp","ChevronLeft","Command","CommandIcon","Option","usePathname","useRouter","formatAdminURL","createContext","Fragment","memo","useCallback","useContext","useEffect","useMemo","useRef","useState","useHotkeys","createDefaultGroups","getCommandMenuAction","CommandEmpty","CommandGroup","CommandInput","CommandItem","CommandList","CommandSeparator","CommandShortcut","LucideIconDynamic","MODAL_SLUG","CommandMenuContext","closeMenu","currentPage","groups","isOpen","items","openMenu","setPage","toggleMenu","useCommandMenu","context","ITEM_SELECTOR","getSelectedElement","containerRef","current","querySelector","CommandMenuComponent","pluginConfig","search","setSearch","submenuItems","setSubmenuItems","isLoadingSubmenu","setIsLoadingSubmenu","isMac","setIsMac","commandListRef","router","pathname","startRouteTransition","t","config","currentCollectionSlug","isDocumentContext","adminRoute","routes","admin","escapedRoute","replace","match","RegExp","isEntryVisible","entry","collectionSlugs","length","includes","collectionContext","currentContext","visibleGroups","filter","group","map","item","visibleItems","test","navigator","platform","submenuEnabled","submenu","enabled","submenuShortcut","shortcut","blurBg","formatShortcutKey","key","parts","split","part","trim","elements","index","lowerPart","toLowerCase","content","size","fetchDocuments","type","searchParam","useAsTitle","encodeURIComponent","response","fetch","slug","data","json","docs","Array","isArray","doc","id","action","href","path","icon","icons","undefined","label","timeoutId","setTimeout","clearTimeout","handleBack","executeItemAction","body","JSON","stringify","headers","method","ok","Error","status","statusText","handler","console","warn","push","openSubmenu","useAsTitleLabel","handleSelect","handleKeyDown","e","preventDefault","stopPropagation","selectedElement","itemType","getAttribute","itemSlug","isShiftPressed","shiftKey","shouldOpenSubmenu","flatMap","g","find","i","document","addEventListener","removeEventListener","placeholder","shouldDisableFilter","footerShortcuts","handleBackdropClick","target","currentTarget","div","className","onClick","shouldFilter","button","span","onValueChange","value","ref","titleName","title","isRenderSeparator","heading","isDynamicIcon","IconComponent","data-action-type","data-item-type","keywords","onSelect","name","kbd","MemoizedCommandMenuComponent","CommandMenuProvider","children","closeModal","isModalOpen","openModal","toggleModal","setCurrentPage","event","i18n","currentLang","language","Provider"],"mappings":"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"}
@@ -1,2 +1,3 @@
1
1
  export { CommandMenuProvider, useCommandMenu } from '../components/CommandMenuContext';
2
2
  export { default as SearchButton } from '../components/SearchButton';
3
+ export { registerCommandMenuAction, unregisterCommandMenuAction, } from '../utils/registry';
@@ -1,4 +1,5 @@
1
1
  export { CommandMenuProvider, useCommandMenu } from '../components/CommandMenuContext';
2
2
  export { default as SearchButton } from '../components/SearchButton';
3
+ export { registerCommandMenuAction, unregisterCommandMenuAction } from '../utils/registry';
3
4
 
4
5
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { CommandMenuProvider, useCommandMenu } from '../components/CommandMenuContext'\nexport { default as SearchButton } from '../components/SearchButton'\n"],"names":["CommandMenuProvider","useCommandMenu","default","SearchButton"],"mappings":"AAAA,SAASA,mBAAmB,EAAEC,cAAc,QAAQ,mCAAkC;AACtF,SAASC,WAAWC,YAAY,QAAQ,6BAA4B"}
1
+ {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { CommandMenuProvider, useCommandMenu } from '../components/CommandMenuContext'\nexport { default as SearchButton } from '../components/SearchButton'\nexport {\n registerCommandMenuAction,\n unregisterCommandMenuAction,\n} from '../utils/registry'\n"],"names":["CommandMenuProvider","useCommandMenu","default","SearchButton","registerCommandMenuAction","unregisterCommandMenuAction"],"mappings":"AAAA,SAASA,mBAAmB,EAAEC,cAAc,QAAQ,mCAAkC;AACtF,SAASC,WAAWC,YAAY,QAAQ,6BAA4B;AACpE,SACEC,yBAAyB,EACzBC,2BAA2B,QACtB,oBAAmB"}
package/dist/types.d.ts CHANGED
@@ -5,12 +5,46 @@ export type LocalizedString = {
5
5
  [locale: string]: string;
6
6
  } | string;
7
7
  export type InternalIcon = IconName | LucideIcon;
8
+ /**
9
+ * Controls which context within a collection a custom item/group is visible in.
10
+ * Pass an array with one or both values:
11
+ *
12
+ * - `'list'` — collection list page (`/admin/collections/{slug}`)
13
+ * - `'document'` — document edit/create page (`/admin/collections/{slug}/{id|create}`)
14
+ *
15
+ * Omit the field (or pass both values) to show in all contexts.
16
+ *
17
+ * @example
18
+ * collectionContext: ['list'] // list page only
19
+ * collectionContext: ['document'] // document page only
20
+ * collectionContext: ['list', 'document'] // both (same as omitting)
21
+ */
22
+ export type CollectionContext = ('document' | 'list')[];
8
23
  /**
9
24
  * Custom menu item, for configuration.
10
25
  * Will be mapped to CommandMenuItem internally.
11
26
  */
12
27
  export type CustomMenuItem = {
13
28
  action: CommandMenuAction;
29
+ /**
30
+ * Restrict visibility to specific contexts within a collection page.
31
+ * Pass an array with one or both values — omit to show in all contexts.
32
+ *
33
+ * - `'list'` — collection list page (`/admin/collections/{slug}`)
34
+ * - `'document'` — document edit/create page (`/admin/collections/{slug}/{id|create}`)
35
+ *
36
+ * @example
37
+ * collectionContext: ['list'] // list page only
38
+ * collectionContext: ['document'] // document page only
39
+ * collectionContext: ['list', 'document'] // both (same as omitting)
40
+ */
41
+ collectionContext?: CollectionContext;
42
+ /**
43
+ * Restrict this item to only appear when the user is on one of these collection pages.
44
+ * Matches against the current route (e.g. `/admin/collections/{slug}`).
45
+ * If omitted or empty, the item appears on all pages.
46
+ */
47
+ collectionSlugs?: CollectionSlug[];
14
48
  icon?: IconName;
15
49
  label: LocalizedString;
16
50
  slug: string;
@@ -23,6 +57,25 @@ export type CustomMenuItem = {
23
57
  * Groups will be merged if they have the same title.
24
58
  */
25
59
  export type CustomMenuGroup = {
60
+ /**
61
+ * Restrict visibility to specific contexts within a collection page.
62
+ * Pass an array with one or both values — omit to show in all contexts.
63
+ *
64
+ * - `'list'` — collection list page (`/admin/collections/{slug}`)
65
+ * - `'document'` — document edit/create page (`/admin/collections/{slug}/{id|create}`)
66
+ *
67
+ * @example
68
+ * collectionContext: ['list'] // list page only
69
+ * collectionContext: ['document'] // document page only
70
+ * collectionContext: ['list', 'document'] // both (same as omitting)
71
+ */
72
+ collectionContext?: CollectionContext;
73
+ /**
74
+ * Restrict this group to only appear when the user is on one of these collection pages.
75
+ * Matches against the current route (e.g. `/admin/collections/{slug}`).
76
+ * If omitted or empty, the group appears on all pages.
77
+ */
78
+ collectionSlugs?: CollectionSlug[];
26
79
  items: CustomMenuItem[];
27
80
  title: LocalizedString;
28
81
  type: 'group';
@@ -148,12 +201,39 @@ export interface CommandMenuActionAPICall {
148
201
  method?: 'DELETE' | 'GET' | 'POST' | 'PUT';
149
202
  type: 'api';
150
203
  }
151
- export type CommandMenuAction = CommandMenuActionAPICall | CommandMenuActionLink;
204
+ /**
205
+ * Calls a function registered via `registerCommandMenuAction(key, fn)` on the client.
206
+ * Since the plugin config is serialized across the server→client boundary, functions
207
+ * cannot be passed directly — use a string key to reference a registered handler instead.
208
+ *
209
+ * @example
210
+ * // In payload.config.ts:
211
+ * action: { type: 'function', key: 'save-current-doc' }
212
+ *
213
+ * // In your client-side code:
214
+ * import { registerCommandMenuAction } from '@veiag/payload-cmdk/client'
215
+ * registerCommandMenuAction('save-current-doc', () => { ... })
216
+ */
217
+ export interface CommandMenuActionFunction {
218
+ /**
219
+ * Key used to look up the registered handler via `registerCommandMenuAction`.
220
+ */
221
+ key: string;
222
+ type: 'function';
223
+ }
224
+ export type CommandMenuAction = CommandMenuActionAPICall | CommandMenuActionFunction | CommandMenuActionLink;
152
225
  export interface CommandMenuItem {
153
226
  /**
154
227
  * Action to perform when the command menu item is selected.
155
228
  */
156
229
  action: CommandMenuAction;
230
+ /** Populated from `CustomMenuItem.collectionContext`. */
231
+ collectionContext?: CollectionContext;
232
+ /**
233
+ * Restrict this item to only appear when the user is on one of these collection pages.
234
+ * Populated from `CustomMenuItem.collectionSlugs`.
235
+ */
236
+ collectionSlugs?: CollectionSlug[];
157
237
  icon?: InternalIcon;
158
238
  label: string;
159
239
  slug: string;
@@ -177,6 +257,13 @@ export interface CommandMenuItem {
177
257
  useAsTitleLabel?: string;
178
258
  }
179
259
  export interface CommandMenuGroup {
260
+ /** Populated from `CustomMenuGroup.collectionContext`. */
261
+ collectionContext?: CollectionContext;
262
+ /**
263
+ * Restrict this group to only appear when the user is on one of these collection pages.
264
+ * Populated from `CustomMenuGroup.collectionSlugs`.
265
+ */
266
+ collectionSlugs?: CollectionSlug[];
180
267
  items: CommandMenuItem[];
181
268
  title: string;
182
269
  }
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { icons, LucideIcon } from 'lucide-react'\nimport type { CollectionSlug, GlobalSlug } from 'payload'\n\nexport type IconName = keyof typeof icons\n\nexport type LocalizedString = { [locale: string]: string } | string\n\nexport type InternalIcon = IconName | LucideIcon\n\n/**\n * Custom menu item, for configuration.\n * Will be mapped to CommandMenuItem internally.\n */\nexport type CustomMenuItem = {\n action: CommandMenuAction\n icon?: IconName\n label: LocalizedString\n slug: string\n type: 'item'\n}\n\n/**\n * Custom menu group, for configuration.\n * Will be mapped to CommandMenuGroup internally.\n *\n * Groups will be merged if they have the same title.\n */\nexport type CustomMenuGroup = {\n items: CustomMenuItem[]\n title: LocalizedString\n type: 'group'\n}\n/**\n * Full serializable config for the plugin.\n */\nexport type PluginCommandMenuConfig = {\n /**\n * Enable backdrop blur effect\n * @default true\n */\n blurBg?: boolean\n /**\n * Custom items or groups to add to the command menu.\n */\n customItems?: (CustomMenuGroup | CustomMenuItem)[]\n /**\n * Disable the plugin functionality\n * @default false\n */\n disabled?: boolean\n /**\n * Custom icons for collections and globals.\n * Key is the collection slug, value is the icon name from lucide-react.\n * Collections default icon - Files,\n * Globals default icon - Globe.\n */\n icons?: {\n /**\n * Custom icons for collections.\n * @default <Files/>\n */\n collections?: Partial<Record<CollectionSlug, IconName>>\n /**\n * Custom icons for globals.\n * @default <Globe/>\n */\n globals?: Partial<Record<GlobalSlug, IconName>>\n }\n /**\n * Configuration for the search button in the admin navigation.\n * Set to false to disable the search button.\n * @default { position: 'actions' }\n */\n searchButton?:\n | {\n /**\n * Position of the search button in the admin navigation.\n * @default 'actions'\n */\n position?: 'actions' | 'nav'\n }\n | false\n /**\n * Keyboard shortcut to open the command menu.\n * Can be a single shortcut string or an array of shortcuts for cross-platform support.\n * @default ['meta+k', 'ctrl+k']\n * @example 'mod+k' or ['meta+k', 'ctrl+k']\n *\n * More details here - https://react-hotkeys-hook.vercel.app/docs/intro\n */\n shortcut?: string | string[]\n /**\n * Specify which collections slugs remove from the command menu.\n * @default ['payload-migrations','payload-preferences','payload-locked-documents']\n *\n * You can also provide an object with `ignoreList` and `replaceDefaults` properties.\n * `replaceDefaults` allows you to completely replace the default slugs to ignore instead of appending to them.\n */\n slugsToIgnore?:\n | {\n /**\n * List of collection/global slugs to ignore in the command menu.\n */\n ignoreList: CollectionSlug[]\n /**\n * Whether to replace the default slugs to ignore instead of appending to them.\n */\n replaceDefaults?: boolean\n }\n | CollectionSlug[]\n /**\n * Configure submenu behavior for collections.\n * When enabled, users can search within a collection's documents.\n * @default { enabled: true, shortcut: 'shift+enter' }\n */\n submenu?: {\n /**\n * Enable or disable submenu functionality.\n * @default true\n */\n enabled?: boolean\n /**\n * Custom icons for collection submenus.\n * Key is the collection slug, value is the icon name from lucide-react.\n *\n * @default null\n */\n icons?: Partial<Record<CollectionSlug, IconName>>\n /**\n * Keyboard shortcut to open collection submenu.\n * - 'shift+enter': Shift+Enter opens submenu, Enter navigates to collection list\n * - 'enter': Enter opens submenu, Shift+Enter navigates to collection list\n * @default 'shift+enter'\n */\n shortcut?: 'enter' | 'shift+enter'\n }\n}\n\nexport interface CommandMenuContextProps {\n children: React.ReactNode\n pluginConfig: PluginCommandMenuConfig\n}\n\nexport interface CommandMenuActionLink {\n href: string\n type: 'link'\n}\n\nexport interface CommandMenuActionAPICall {\n body?: {\n [key: string]: unknown\n }\n href: string\n /**\n * HTTP method to use for the API call.\n * @default 'GET'\n */\n method?: 'DELETE' | 'GET' | 'POST' | 'PUT'\n type: 'api'\n}\n\nexport type CommandMenuAction = CommandMenuActionAPICall | CommandMenuActionLink\n\nexport interface CommandMenuItem {\n /**\n * Action to perform when the command menu item is selected.\n */\n action: CommandMenuAction\n icon?: InternalIcon\n label: string\n slug: string\n /**\n * Type of the command menu item. Used for grouping and icons.\n * @default 'custom'\n */\n type: 'collection' | 'custom' | 'global'\n\n /**\n * Field name used as title for collection documents.\n * Only applicable for collection type items.\n * Defaults to 'id' if not specified.\n */\n useAsTitle?: string\n /**\n * Label for the field used as title for collection documents.\n * Only applicable for collection type items.\n *\n * Used in submenu search placeholder.\n */\n useAsTitleLabel?: string\n}\n\nexport interface CommandMenuGroup {\n items: CommandMenuItem[]\n title: string\n}\n\n/**\n * Page state for command menu navigation.\n * - 'main': Default view showing all collections/globals/custom items\n * - CollectionSearchPage: Submenu view for searching within a specific collection\n */\nexport type CommandMenuPage =\n | 'main'\n | {\n /**\n * Collection label for display\n */\n label: string\n /**\n * Collection slug\n */\n slug: string\n /**\n * Page type identifier\n */\n type: 'collection-search'\n /**\n * Field name to use as document title\n */\n useAsTitle: string\n /**\n * Label for the field used as title\n */\n useAsTitleLabel: string\n }\n\n/**\n * Generic document type for collections, with dynamic keys.\n * We assume values are either string or number for simplicity, useAsTitle is making sure of that.\n */\nexport type GenericCollectionDocument = {\n [key: string]: number | string\n id: string\n}\n"],"names":[],"mappings":"AAmOA;;;CAGC,GACD,WAGC"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { icons, LucideIcon } from 'lucide-react'\nimport type { CollectionSlug, GlobalSlug } from 'payload'\n\nexport type IconName = keyof typeof icons\n\nexport type LocalizedString = { [locale: string]: string } | string\n\nexport type InternalIcon = IconName | LucideIcon\n\n/**\n * Controls which context within a collection a custom item/group is visible in.\n * Pass an array with one or both values:\n *\n * - `'list'` — collection list page (`/admin/collections/{slug}`)\n * - `'document'` — document edit/create page (`/admin/collections/{slug}/{id|create}`)\n *\n * Omit the field (or pass both values) to show in all contexts.\n *\n * @example\n * collectionContext: ['list'] // list page only\n * collectionContext: ['document'] // document page only\n * collectionContext: ['list', 'document'] // both (same as omitting)\n */\nexport type CollectionContext = ('document' | 'list')[]\n\n/**\n * Custom menu item, for configuration.\n * Will be mapped to CommandMenuItem internally.\n */\nexport type CustomMenuItem = {\n action: CommandMenuAction\n /**\n * Restrict visibility to specific contexts within a collection page.\n * Pass an array with one or both values — omit to show in all contexts.\n *\n * - `'list'` — collection list page (`/admin/collections/{slug}`)\n * - `'document'` — document edit/create page (`/admin/collections/{slug}/{id|create}`)\n *\n * @example\n * collectionContext: ['list'] // list page only\n * collectionContext: ['document'] // document page only\n * collectionContext: ['list', 'document'] // both (same as omitting)\n */\n collectionContext?: CollectionContext\n /**\n * Restrict this item to only appear when the user is on one of these collection pages.\n * Matches against the current route (e.g. `/admin/collections/{slug}`).\n * If omitted or empty, the item appears on all pages.\n */\n collectionSlugs?: CollectionSlug[]\n icon?: IconName\n label: LocalizedString\n slug: string\n type: 'item'\n}\n\n/**\n * Custom menu group, for configuration.\n * Will be mapped to CommandMenuGroup internally.\n *\n * Groups will be merged if they have the same title.\n */\nexport type CustomMenuGroup = {\n /**\n * Restrict visibility to specific contexts within a collection page.\n * Pass an array with one or both values — omit to show in all contexts.\n *\n * - `'list'` — collection list page (`/admin/collections/{slug}`)\n * - `'document'` — document edit/create page (`/admin/collections/{slug}/{id|create}`)\n *\n * @example\n * collectionContext: ['list'] // list page only\n * collectionContext: ['document'] // document page only\n * collectionContext: ['list', 'document'] // both (same as omitting)\n */\n collectionContext?: CollectionContext\n /**\n * Restrict this group to only appear when the user is on one of these collection pages.\n * Matches against the current route (e.g. `/admin/collections/{slug}`).\n * If omitted or empty, the group appears on all pages.\n */\n collectionSlugs?: CollectionSlug[]\n items: CustomMenuItem[]\n title: LocalizedString\n type: 'group'\n}\n/**\n * Full serializable config for the plugin.\n */\nexport type PluginCommandMenuConfig = {\n /**\n * Enable backdrop blur effect\n * @default true\n */\n blurBg?: boolean\n /**\n * Custom items or groups to add to the command menu.\n */\n customItems?: (CustomMenuGroup | CustomMenuItem)[]\n /**\n * Disable the plugin functionality\n * @default false\n */\n disabled?: boolean\n /**\n * Custom icons for collections and globals.\n * Key is the collection slug, value is the icon name from lucide-react.\n * Collections default icon - Files,\n * Globals default icon - Globe.\n */\n icons?: {\n /**\n * Custom icons for collections.\n * @default <Files/>\n */\n collections?: Partial<Record<CollectionSlug, IconName>>\n /**\n * Custom icons for globals.\n * @default <Globe/>\n */\n globals?: Partial<Record<GlobalSlug, IconName>>\n }\n /**\n * Configuration for the search button in the admin navigation.\n * Set to false to disable the search button.\n * @default { position: 'actions' }\n */\n searchButton?:\n | {\n /**\n * Position of the search button in the admin navigation.\n * @default 'actions'\n */\n position?: 'actions' | 'nav'\n }\n | false\n /**\n * Keyboard shortcut to open the command menu.\n * Can be a single shortcut string or an array of shortcuts for cross-platform support.\n * @default ['meta+k', 'ctrl+k']\n * @example 'mod+k' or ['meta+k', 'ctrl+k']\n *\n * More details here - https://react-hotkeys-hook.vercel.app/docs/intro\n */\n shortcut?: string | string[]\n /**\n * Specify which collections slugs remove from the command menu.\n * @default ['payload-migrations','payload-preferences','payload-locked-documents']\n *\n * You can also provide an object with `ignoreList` and `replaceDefaults` properties.\n * `replaceDefaults` allows you to completely replace the default slugs to ignore instead of appending to them.\n */\n slugsToIgnore?:\n | {\n /**\n * List of collection/global slugs to ignore in the command menu.\n */\n ignoreList: CollectionSlug[]\n /**\n * Whether to replace the default slugs to ignore instead of appending to them.\n */\n replaceDefaults?: boolean\n }\n | CollectionSlug[]\n /**\n * Configure submenu behavior for collections.\n * When enabled, users can search within a collection's documents.\n * @default { enabled: true, shortcut: 'shift+enter' }\n */\n submenu?: {\n /**\n * Enable or disable submenu functionality.\n * @default true\n */\n enabled?: boolean\n /**\n * Custom icons for collection submenus.\n * Key is the collection slug, value is the icon name from lucide-react.\n *\n * @default null\n */\n icons?: Partial<Record<CollectionSlug, IconName>>\n /**\n * Keyboard shortcut to open collection submenu.\n * - 'shift+enter': Shift+Enter opens submenu, Enter navigates to collection list\n * - 'enter': Enter opens submenu, Shift+Enter navigates to collection list\n * @default 'shift+enter'\n */\n shortcut?: 'enter' | 'shift+enter'\n }\n}\n\nexport interface CommandMenuContextProps {\n children: React.ReactNode\n pluginConfig: PluginCommandMenuConfig\n}\n\nexport interface CommandMenuActionLink {\n href: string\n type: 'link'\n}\n\nexport interface CommandMenuActionAPICall {\n body?: {\n [key: string]: unknown\n }\n href: string\n /**\n * HTTP method to use for the API call.\n * @default 'GET'\n */\n method?: 'DELETE' | 'GET' | 'POST' | 'PUT'\n type: 'api'\n}\n\n/**\n * Calls a function registered via `registerCommandMenuAction(key, fn)` on the client.\n * Since the plugin config is serialized across the server→client boundary, functions\n * cannot be passed directly — use a string key to reference a registered handler instead.\n *\n * @example\n * // In payload.config.ts:\n * action: { type: 'function', key: 'save-current-doc' }\n *\n * // In your client-side code:\n * import { registerCommandMenuAction } from '@veiag/payload-cmdk/client'\n * registerCommandMenuAction('save-current-doc', () => { ... })\n */\nexport interface CommandMenuActionFunction {\n /**\n * Key used to look up the registered handler via `registerCommandMenuAction`.\n */\n key: string\n type: 'function'\n}\n\nexport type CommandMenuAction =\n | CommandMenuActionAPICall\n | CommandMenuActionFunction\n | CommandMenuActionLink\n\nexport interface CommandMenuItem {\n /**\n * Action to perform when the command menu item is selected.\n */\n action: CommandMenuAction\n /** Populated from `CustomMenuItem.collectionContext`. */\n collectionContext?: CollectionContext\n /**\n * Restrict this item to only appear when the user is on one of these collection pages.\n * Populated from `CustomMenuItem.collectionSlugs`.\n */\n collectionSlugs?: CollectionSlug[]\n icon?: InternalIcon\n label: string\n slug: string\n /**\n * Type of the command menu item. Used for grouping and icons.\n * @default 'custom'\n */\n type: 'collection' | 'custom' | 'global'\n\n /**\n * Field name used as title for collection documents.\n * Only applicable for collection type items.\n * Defaults to 'id' if not specified.\n */\n useAsTitle?: string\n /**\n * Label for the field used as title for collection documents.\n * Only applicable for collection type items.\n *\n * Used in submenu search placeholder.\n */\n useAsTitleLabel?: string\n}\n\nexport interface CommandMenuGroup {\n /** Populated from `CustomMenuGroup.collectionContext`. */\n collectionContext?: CollectionContext\n /**\n * Restrict this group to only appear when the user is on one of these collection pages.\n * Populated from `CustomMenuGroup.collectionSlugs`.\n */\n collectionSlugs?: CollectionSlug[]\n items: CommandMenuItem[]\n title: string\n}\n\n/**\n * Page state for command menu navigation.\n * - 'main': Default view showing all collections/globals/custom items\n * - CollectionSearchPage: Submenu view for searching within a specific collection\n */\nexport type CommandMenuPage =\n | 'main'\n | {\n /**\n * Collection label for display\n */\n label: string\n /**\n * Collection slug\n */\n slug: string\n /**\n * Page type identifier\n */\n type: 'collection-search'\n /**\n * Field name to use as document title\n */\n useAsTitle: string\n /**\n * Label for the field used as title\n */\n useAsTitleLabel: string\n }\n\n/**\n * Generic document type for collections, with dynamic keys.\n * We assume values are either string or number for simplicity, useAsTitle is making sure of that.\n */\nexport type GenericCollectionDocument = {\n [key: string]: number | string\n id: string\n}\n"],"names":[],"mappings":"AA+TA;;;CAGC,GACD,WAGC"}
@@ -1,4 +1,5 @@
1
1
  import { Files, Globe } from 'lucide-react';
2
+ import { formatAdminURL } from 'payload/shared';
2
3
  export const convertSlugToTitle = (slug)=>{
3
4
  return slug.replace(/-/g, ' ').replace(/\b\w/g, (char)=>char.toUpperCase());
4
5
  };
@@ -39,12 +40,16 @@ export const convertConfigItem = (item, currentLang)=>{
39
40
  slug: item.slug,
40
41
  type: 'custom',
41
42
  action: item.action,
43
+ collectionContext: item.collectionContext,
44
+ collectionSlugs: item.collectionSlugs,
42
45
  icon: item.icon,
43
46
  label: extractLocalizedValue(item.label, currentLang, item.slug)
44
47
  };
45
48
  };
46
49
  export const convertConfigGroup = (group, currentLang)=>{
47
50
  return {
51
+ collectionContext: group.collectionContext,
52
+ collectionSlugs: group.collectionSlugs,
48
53
  items: group.items.map((item)=>convertConfigItem(item, currentLang)),
49
54
  title: extractLocalizedValue(group.title, currentLang)
50
55
  };
@@ -55,7 +60,10 @@ export const convertConfigGroup = (group, currentLang)=>{
55
60
  */ const DEFAULT_SLUGS_TO_IGNORE = [
56
61
  'payload-migrations',
57
62
  'payload-preferences',
58
- 'payload-locked-documents'
63
+ 'payload-locked-documents',
64
+ 'payload-folders',
65
+ 'payload-kvs',
66
+ 'payload-jobs'
59
67
  ];
60
68
  export const createDefaultGroups = (config, currentLang, pluginConfig)=>{
61
69
  const groups = [];
@@ -65,6 +73,7 @@ export const createDefaultGroups = (config, currentLang, pluginConfig)=>{
65
73
  let slugsToIgnore = [
66
74
  ...DEFAULT_SLUGS_TO_IGNORE
67
75
  ];
76
+ const adminRoute = config.routes?.admin ?? '/admin';
68
77
  //Handle slugs to ignore from plugin config
69
78
  if (pluginConfig?.slugsToIgnore) {
70
79
  if (Array.isArray(pluginConfig.slugsToIgnore)) {
@@ -113,7 +122,10 @@ export const createDefaultGroups = (config, currentLang, pluginConfig)=>{
113
122
  type: 'collection',
114
123
  action: {
115
124
  type: 'link',
116
- href: `/admin/collections/${collection.slug}`
125
+ href: formatAdminURL({
126
+ adminRoute,
127
+ path: `/collections/${collection.slug}`
128
+ })
117
129
  },
118
130
  //Either custom icon from plugin config, or default Files icon
119
131
  icon: pluginConfig?.icons?.collections?.[collection.slug] || Files,
@@ -146,7 +158,10 @@ export const createDefaultGroups = (config, currentLang, pluginConfig)=>{
146
158
  type: 'global',
147
159
  action: {
148
160
  type: 'link',
149
- href: `/admin/globals/${global.slug}`
161
+ href: formatAdminURL({
162
+ adminRoute,
163
+ path: `/globals/${global.slug}`
164
+ })
150
165
  },
151
166
  //Either custom icon from plugin config, or default Globe icon
152
167
  icon: pluginConfig?.icons?.globals?.[global.slug] || Globe,
@@ -165,6 +180,8 @@ export const createDefaultGroups = (config, currentLang, pluginConfig)=>{
165
180
  if (!avaibleGroups.has(convertedGroup.title)) {
166
181
  avaibleGroups.add(convertedGroup.title);
167
182
  groups.push({
183
+ collectionContext: convertedGroup.collectionContext,
184
+ collectionSlugs: convertedGroup.collectionSlugs,
168
185
  items: [],
169
186
  title: convertedGroup.title
170
187
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/index.ts"],"sourcesContent":["import type { ClientConfig, LabelFunction, TextFieldClient } from 'payload'\nimport type {\n CommandMenuGroup,\n CommandMenuItem,\n CustomMenuGroup,\n CustomMenuItem,\n LocalizedString,\n PluginCommandMenuConfig,\n} from 'src/types'\n\nimport { Files, Globe } from 'lucide-react'\n\nexport const convertSlugToTitle = (slug: string): string => {\n return slug.replace(/-/g, ' ').replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nexport const extractLocalizedValue = (\n value: LocalizedString,\n locale: string,\n slug?: string,\n): string => {\n if (typeof value === 'string') {\n return value\n }\n return value[locale] || convertSlugToTitle(slug || '')\n}\n\nexport const extractLocalizedCollectionName = (\n collection: {\n labels?: { plural?: LocalizedString; singular?: LocalizedString }\n slug: string\n },\n locale: string,\n): string => {\n //Get plural name if exists, otherwise singular, otherwise slug\n if (collection.labels?.plural) {\n return extractLocalizedValue(collection.labels.plural, locale, collection.slug)\n }\n if (collection.labels?.singular) {\n return extractLocalizedValue(collection.labels.singular, locale, collection.slug)\n }\n return '' //Generally should not happen\n}\n\nexport const extractLocalizedGlobalName = (\n global: {\n label?: LabelFunction | LocalizedString\n slug: string\n },\n locale: string,\n): string => {\n if (global.label) {\n return extractLocalizedValue(\n //Ignore label functions, they are not serializable\n typeof global.label === 'function' ? {} : global.label,\n locale,\n global.slug,\n )\n }\n return '' //Generally should not happen\n}\n\nexport const extractLocalizedGroupName = (\n object: {\n admin?: { group?: false | LocalizedString }\n },\n locale: string,\n): null | string => {\n if (object.admin?.group) {\n //Try to extract group name, with fallback to null (no group)\n return extractLocalizedValue(object.admin.group, locale)?.trim() || null\n }\n return null\n}\n\nexport const convertConfigItem = (item: CustomMenuItem, currentLang: string): CommandMenuItem => {\n return {\n slug: item.slug,\n type: 'custom',\n action: item.action,\n icon: item.icon,\n label: extractLocalizedValue(item.label, currentLang, item.slug),\n }\n}\n\nexport const convertConfigGroup = (\n group: CustomMenuGroup,\n currentLang: string,\n): CommandMenuGroup => {\n return {\n items: group.items.map((item) => convertConfigItem(item, currentLang)), // Will be merged with existing items if group exists\n title: extractLocalizedValue(group.title, currentLang),\n }\n}\n/**\n * Set of collection/globals slugs to ignore in the command menu.\n * This is useful to avoid showing certain collections/globals in the command menu.\n */\nconst DEFAULT_SLUGS_TO_IGNORE: string[] = [\n 'payload-migrations',\n 'payload-preferences',\n 'payload-locked-documents',\n]\nexport const createDefaultGroups = (\n config: ClientConfig,\n currentLang: string,\n pluginConfig: PluginCommandMenuConfig,\n): {\n groups: CommandMenuGroup[]\n /**\n * Stray items that don't belong to any group.\n * Only with custom items.\n */\n items: CommandMenuItem[]\n} => {\n const groups: CommandMenuGroup[] = []\n const items: CommandMenuItem[] = []\n const avaibleGroups = new Set<string>() //To avoid duplicates\n let slugsToIgnore = [...DEFAULT_SLUGS_TO_IGNORE]\n\n //Handle slugs to ignore from plugin config\n if (pluginConfig?.slugsToIgnore) {\n if (Array.isArray(pluginConfig.slugsToIgnore)) {\n slugsToIgnore.push(...pluginConfig.slugsToIgnore)\n } else {\n //Object with ignoreList and replaceDefaults\n if (pluginConfig.slugsToIgnore.replaceDefaults) {\n //Replace defaults\n slugsToIgnore = [] //Reset\n }\n slugsToIgnore.push(...pluginConfig.slugsToIgnore.ignoreList)\n }\n }\n\n if (config.collections) {\n config.collections.forEach((collection) => {\n if (slugsToIgnore.includes(collection.slug)) {\n return\n }\n\n const groupName = extractLocalizedGroupName(collection, currentLang) || 'Collections'\n // console.log(collection.slug, 'groupName:', groupName, 'Object', collection.admin)\n if (!avaibleGroups.has(groupName)) {\n avaibleGroups.add(groupName)\n groups.push({\n items: [],\n title: groupName,\n })\n }\n const group = groups.find((g) => g.title === groupName)\n if (group) {\n const useAsTitleField = collection.admin?.useAsTitle || 'id'\n let useAsTitleFieldLabel: TextFieldClient['label'] | undefined = undefined\n let useAsTitleLabel: string | undefined = undefined\n //Only extract useAsTitle label if submenu is enabled\n if (pluginConfig?.submenu?.enabled !== false) {\n useAsTitleFieldLabel = (\n collection?.fields?.find((field) => {\n if ('name' in field === false) {\n return null\n }\n return field.name === useAsTitleField\n }) as null | TextFieldClient\n )?.label\n //Extract label for useAsTitle field\n useAsTitleLabel = extractLocalizedValue(\n typeof useAsTitleFieldLabel === 'function' ? {} : useAsTitleFieldLabel || {},\n currentLang,\n useAsTitleField,\n )\n }\n group.items.push({\n slug: collection.slug,\n type: 'collection',\n action: {\n type: 'link',\n href: `/admin/collections/${collection.slug}`,\n },\n //Either custom icon from plugin config, or default Files icon\n icon: pluginConfig?.icons?.collections?.[collection.slug] || Files,\n label: extractLocalizedCollectionName(collection, currentLang),\n useAsTitle: useAsTitleField,\n useAsTitleLabel: useAsTitleLabel || useAsTitleField,\n })\n }\n })\n }\n //Globals\n if (config.globals) {\n config.globals.forEach((global) => {\n if (slugsToIgnore.includes(global.slug)) {\n return\n }\n //Same logic as collections\n const groupName = extractLocalizedGroupName(global, currentLang) || 'Globals'\n if (!avaibleGroups.has(groupName)) {\n avaibleGroups.add(groupName)\n groups.push({\n items: [],\n title: groupName,\n })\n }\n const group = groups.find((g) => g.title === groupName)\n if (group) {\n group.items.push({\n slug: global.slug,\n type: 'global',\n action: {\n type: 'link',\n href: `/admin/globals/${global.slug}`,\n },\n //Either custom icon from plugin config, or default Globe icon\n icon: pluginConfig?.icons?.globals?.[global.slug] || Globe,\n label: extractLocalizedGlobalName(global, currentLang),\n })\n }\n })\n }\n //Now custom items/groups from plugin config\n if (pluginConfig?.customItems) {\n //We are not using slugsToIgnore for custom items, as they are user-defined\n pluginConfig.customItems.forEach((value) => {\n if (value.type === 'group') {\n const convertedGroup = convertConfigGroup(value, currentLang)\n //Check if group already exists using our set\n if (!avaibleGroups.has(convertedGroup.title)) {\n avaibleGroups.add(convertedGroup.title)\n groups.push({\n items: [], //Don't add items yet, will do below\n title: convertedGroup.title,\n })\n }\n const group = groups.find((g) => g.title === convertedGroup.title)\n if (group) {\n //Append items to existing group, or if it was empty - add items\n group.items.push(...convertedGroup.items)\n }\n }\n if (value.type === 'item') {\n //Stray item, add to items array\n const convertedItem = convertConfigItem(value, currentLang)\n items.push(convertedItem)\n }\n })\n }\n return { groups, items }\n}\n"],"names":["Files","Globe","convertSlugToTitle","slug","replace","char","toUpperCase","extractLocalizedValue","value","locale","extractLocalizedCollectionName","collection","labels","plural","singular","extractLocalizedGlobalName","global","label","extractLocalizedGroupName","object","admin","group","trim","convertConfigItem","item","currentLang","type","action","icon","convertConfigGroup","items","map","title","DEFAULT_SLUGS_TO_IGNORE","createDefaultGroups","config","pluginConfig","groups","avaibleGroups","Set","slugsToIgnore","Array","isArray","push","replaceDefaults","ignoreList","collections","forEach","includes","groupName","has","add","find","g","useAsTitleField","useAsTitle","useAsTitleFieldLabel","undefined","useAsTitleLabel","submenu","enabled","fields","field","name","href","icons","globals","customItems","convertedGroup","convertedItem"],"mappings":"AAUA,SAASA,KAAK,EAAEC,KAAK,QAAQ,eAAc;AAE3C,OAAO,MAAMC,qBAAqB,CAACC;IACjC,OAAOA,KAAKC,OAAO,CAAC,MAAM,KAAKA,OAAO,CAAC,SAAS,CAACC,OAASA,KAAKC,WAAW;AAC5E,EAAC;AAED,OAAO,MAAMC,wBAAwB,CACnCC,OACAC,QACAN;IAEA,IAAI,OAAOK,UAAU,UAAU;QAC7B,OAAOA;IACT;IACA,OAAOA,KAAK,CAACC,OAAO,IAAIP,mBAAmBC,QAAQ;AACrD,EAAC;AAED,OAAO,MAAMO,iCAAiC,CAC5CC,YAIAF;IAEA,+DAA+D;IAC/D,IAAIE,WAAWC,MAAM,EAAEC,QAAQ;QAC7B,OAAON,sBAAsBI,WAAWC,MAAM,CAACC,MAAM,EAAEJ,QAAQE,WAAWR,IAAI;IAChF;IACA,IAAIQ,WAAWC,MAAM,EAAEE,UAAU;QAC/B,OAAOP,sBAAsBI,WAAWC,MAAM,CAACE,QAAQ,EAAEL,QAAQE,WAAWR,IAAI;IAClF;IACA,OAAO,GAAG,6BAA6B;;AACzC,EAAC;AAED,OAAO,MAAMY,6BAA6B,CACxCC,QAIAP;IAEA,IAAIO,OAAOC,KAAK,EAAE;QAChB,OAAOV,sBACL,mDAAmD;QACnD,OAAOS,OAAOC,KAAK,KAAK,aAAa,CAAC,IAAID,OAAOC,KAAK,EACtDR,QACAO,OAAOb,IAAI;IAEf;IACA,OAAO,GAAG,6BAA6B;;AACzC,EAAC;AAED,OAAO,MAAMe,4BAA4B,CACvCC,QAGAV;IAEA,IAAIU,OAAOC,KAAK,EAAEC,OAAO;QACvB,6DAA6D;QAC7D,OAAOd,sBAAsBY,OAAOC,KAAK,CAACC,KAAK,EAAEZ,SAASa,UAAU;IACtE;IACA,OAAO;AACT,EAAC;AAED,OAAO,MAAMC,oBAAoB,CAACC,MAAsBC;IACtD,OAAO;QACLtB,MAAMqB,KAAKrB,IAAI;QACfuB,MAAM;QACNC,QAAQH,KAAKG,MAAM;QACnBC,MAAMJ,KAAKI,IAAI;QACfX,OAAOV,sBAAsBiB,KAAKP,KAAK,EAAEQ,aAAaD,KAAKrB,IAAI;IACjE;AACF,EAAC;AAED,OAAO,MAAM0B,qBAAqB,CAChCR,OACAI;IAEA,OAAO;QACLK,OAAOT,MAAMS,KAAK,CAACC,GAAG,CAAC,CAACP,OAASD,kBAAkBC,MAAMC;QACzDO,OAAOzB,sBAAsBc,MAAMW,KAAK,EAAEP;IAC5C;AACF,EAAC;AACD;;;CAGC,GACD,MAAMQ,0BAAoC;IACxC;IACA;IACA;CACD;AACD,OAAO,MAAMC,sBAAsB,CACjCC,QACAV,aACAW;IASA,MAAMC,SAA6B,EAAE;IACrC,MAAMP,QAA2B,EAAE;IACnC,MAAMQ,gBAAgB,IAAIC,MAAc,qBAAqB;;IAC7D,IAAIC,gBAAgB;WAAIP;KAAwB;IAEhD,2CAA2C;IAC3C,IAAIG,cAAcI,eAAe;QAC/B,IAAIC,MAAMC,OAAO,CAACN,aAAaI,aAAa,GAAG;YAC7CA,cAAcG,IAAI,IAAIP,aAAaI,aAAa;QAClD,OAAO;YACL,4CAA4C;YAC5C,IAAIJ,aAAaI,aAAa,CAACI,eAAe,EAAE;gBAC9C,kBAAkB;gBAClBJ,gBAAgB,EAAE,EAAC,OAAO;YAC5B;YACAA,cAAcG,IAAI,IAAIP,aAAaI,aAAa,CAACK,UAAU;QAC7D;IACF;IAEA,IAAIV,OAAOW,WAAW,EAAE;QACtBX,OAAOW,WAAW,CAACC,OAAO,CAAC,CAACpC;YAC1B,IAAI6B,cAAcQ,QAAQ,CAACrC,WAAWR,IAAI,GAAG;gBAC3C;YACF;YAEA,MAAM8C,YAAY/B,0BAA0BP,YAAYc,gBAAgB;YACxE,sFAAsF;YACtF,IAAI,CAACa,cAAcY,GAAG,CAACD,YAAY;gBACjCX,cAAca,GAAG,CAACF;gBAClBZ,OAAOM,IAAI,CAAC;oBACVb,OAAO,EAAE;oBACTE,OAAOiB;gBACT;YACF;YACA,MAAM5B,QAAQgB,OAAOe,IAAI,CAAC,CAACC,IAAMA,EAAErB,KAAK,KAAKiB;YAC7C,IAAI5B,OAAO;gBACT,MAAMiC,kBAAkB3C,WAAWS,KAAK,EAAEmC,cAAc;gBACxD,IAAIC,uBAA6DC;gBACjE,IAAIC,kBAAsCD;gBAC1C,qDAAqD;gBACrD,IAAIrB,cAAcuB,SAASC,YAAY,OAAO;oBAC5CJ,uBACE7C,YAAYkD,QAAQT,KAAK,CAACU;wBACxB,IAAI,UAAUA,UAAU,OAAO;4BAC7B,OAAO;wBACT;wBACA,OAAOA,MAAMC,IAAI,KAAKT;oBACxB,IACCrC;oBACH,oCAAoC;oBACpCyC,kBAAkBnD,sBAChB,OAAOiD,yBAAyB,aAAa,CAAC,IAAIA,wBAAwB,CAAC,GAC3E/B,aACA6B;gBAEJ;gBACAjC,MAAMS,KAAK,CAACa,IAAI,CAAC;oBACfxC,MAAMQ,WAAWR,IAAI;oBACrBuB,MAAM;oBACNC,QAAQ;wBACND,MAAM;wBACNsC,MAAM,CAAC,mBAAmB,EAAErD,WAAWR,IAAI,EAAE;oBAC/C;oBACA,8DAA8D;oBAC9DyB,MAAMQ,cAAc6B,OAAOnB,aAAa,CAACnC,WAAWR,IAAI,CAAC,IAAIH;oBAC7DiB,OAAOP,+BAA+BC,YAAYc;oBAClD8B,YAAYD;oBACZI,iBAAiBA,mBAAmBJ;gBACtC;YACF;QACF;IACF;IACA,SAAS;IACT,IAAInB,OAAO+B,OAAO,EAAE;QAClB/B,OAAO+B,OAAO,CAACnB,OAAO,CAAC,CAAC/B;YACtB,IAAIwB,cAAcQ,QAAQ,CAAChC,OAAOb,IAAI,GAAG;gBACvC;YACF;YACA,2BAA2B;YAC3B,MAAM8C,YAAY/B,0BAA0BF,QAAQS,gBAAgB;YACpE,IAAI,CAACa,cAAcY,GAAG,CAACD,YAAY;gBACjCX,cAAca,GAAG,CAACF;gBAClBZ,OAAOM,IAAI,CAAC;oBACVb,OAAO,EAAE;oBACTE,OAAOiB;gBACT;YACF;YACA,MAAM5B,QAAQgB,OAAOe,IAAI,CAAC,CAACC,IAAMA,EAAErB,KAAK,KAAKiB;YAC7C,IAAI5B,OAAO;gBACTA,MAAMS,KAAK,CAACa,IAAI,CAAC;oBACfxC,MAAMa,OAAOb,IAAI;oBACjBuB,MAAM;oBACNC,QAAQ;wBACND,MAAM;wBACNsC,MAAM,CAAC,eAAe,EAAEhD,OAAOb,IAAI,EAAE;oBACvC;oBACA,8DAA8D;oBAC9DyB,MAAMQ,cAAc6B,OAAOC,SAAS,CAAClD,OAAOb,IAAI,CAAC,IAAIF;oBACrDgB,OAAOF,2BAA2BC,QAAQS;gBAC5C;YACF;QACF;IACF;IACA,4CAA4C;IAC5C,IAAIW,cAAc+B,aAAa;QAC7B,2EAA2E;QAC3E/B,aAAa+B,WAAW,CAACpB,OAAO,CAAC,CAACvC;YAChC,IAAIA,MAAMkB,IAAI,KAAK,SAAS;gBAC1B,MAAM0C,iBAAiBvC,mBAAmBrB,OAAOiB;gBACjD,6CAA6C;gBAC7C,IAAI,CAACa,cAAcY,GAAG,CAACkB,eAAepC,KAAK,GAAG;oBAC5CM,cAAca,GAAG,CAACiB,eAAepC,KAAK;oBACtCK,OAAOM,IAAI,CAAC;wBACVb,OAAO,EAAE;wBACTE,OAAOoC,eAAepC,KAAK;oBAC7B;gBACF;gBACA,MAAMX,QAAQgB,OAAOe,IAAI,CAAC,CAACC,IAAMA,EAAErB,KAAK,KAAKoC,eAAepC,KAAK;gBACjE,IAAIX,OAAO;oBACT,gEAAgE;oBAChEA,MAAMS,KAAK,CAACa,IAAI,IAAIyB,eAAetC,KAAK;gBAC1C;YACF;YACA,IAAItB,MAAMkB,IAAI,KAAK,QAAQ;gBACzB,gCAAgC;gBAChC,MAAM2C,gBAAgB9C,kBAAkBf,OAAOiB;gBAC/CK,MAAMa,IAAI,CAAC0B;YACb;QACF;IACF;IACA,OAAO;QAAEhC;QAAQP;IAAM;AACzB,EAAC"}
1
+ {"version":3,"sources":["../../src/utils/index.ts"],"sourcesContent":["import type { ClientConfig, LabelFunction, TextFieldClient } from 'payload'\nimport type {\n CommandMenuGroup,\n CommandMenuItem,\n CustomMenuGroup,\n CustomMenuItem,\n LocalizedString,\n PluginCommandMenuConfig,\n} from 'src/types'\n\nimport { Files, Globe } from 'lucide-react'\nimport { formatAdminURL } from 'payload/shared'\n\nexport const convertSlugToTitle = (slug: string): string => {\n return slug.replace(/-/g, ' ').replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nexport const extractLocalizedValue = (\n value: LocalizedString,\n locale: string,\n slug?: string,\n): string => {\n if (typeof value === 'string') {\n return value\n }\n return value[locale] || convertSlugToTitle(slug || '')\n}\n\nexport const extractLocalizedCollectionName = (\n collection: {\n labels?: { plural?: LocalizedString; singular?: LocalizedString }\n slug: string\n },\n locale: string,\n): string => {\n //Get plural name if exists, otherwise singular, otherwise slug\n if (collection.labels?.plural) {\n return extractLocalizedValue(collection.labels.plural, locale, collection.slug)\n }\n if (collection.labels?.singular) {\n return extractLocalizedValue(collection.labels.singular, locale, collection.slug)\n }\n return '' //Generally should not happen\n}\n\nexport const extractLocalizedGlobalName = (\n global: {\n label?: LabelFunction | LocalizedString\n slug: string\n },\n locale: string,\n): string => {\n if (global.label) {\n return extractLocalizedValue(\n //Ignore label functions, they are not serializable\n typeof global.label === 'function' ? {} : global.label,\n locale,\n global.slug,\n )\n }\n return '' //Generally should not happen\n}\n\nexport const extractLocalizedGroupName = (\n object: {\n admin?: { group?: false | LocalizedString }\n },\n locale: string,\n): null | string => {\n if (object.admin?.group) {\n //Try to extract group name, with fallback to null (no group)\n return extractLocalizedValue(object.admin.group, locale)?.trim() || null\n }\n return null\n}\n\nexport const convertConfigItem = (item: CustomMenuItem, currentLang: string): CommandMenuItem => {\n return {\n slug: item.slug,\n type: 'custom',\n action: item.action,\n collectionContext: item.collectionContext,\n collectionSlugs: item.collectionSlugs,\n icon: item.icon,\n label: extractLocalizedValue(item.label, currentLang, item.slug),\n }\n}\n\nexport const convertConfigGroup = (\n group: CustomMenuGroup,\n currentLang: string,\n): CommandMenuGroup => {\n return {\n collectionContext: group.collectionContext,\n collectionSlugs: group.collectionSlugs,\n items: group.items.map((item) => convertConfigItem(item, currentLang)), // Will be merged with existing items if group exists\n title: extractLocalizedValue(group.title, currentLang),\n }\n}\n/**\n * Set of collection/globals slugs to ignore in the command menu.\n * This is useful to avoid showing certain collections/globals in the command menu.\n */\nconst DEFAULT_SLUGS_TO_IGNORE: string[] = [\n 'payload-migrations',\n 'payload-preferences',\n 'payload-locked-documents',\n 'payload-folders',\n 'payload-kvs',\n 'payload-jobs',\n]\nexport const createDefaultGroups = (\n config: ClientConfig,\n currentLang: string,\n pluginConfig: PluginCommandMenuConfig,\n): {\n groups: CommandMenuGroup[]\n /**\n * Stray items that don't belong to any group.\n * Only with custom items.\n */\n items: CommandMenuItem[]\n} => {\n const groups: CommandMenuGroup[] = []\n const items: CommandMenuItem[] = []\n const avaibleGroups = new Set<string>() //To avoid duplicates\n let slugsToIgnore = [...DEFAULT_SLUGS_TO_IGNORE]\n\n const adminRoute = config.routes?.admin ?? '/admin'\n\n //Handle slugs to ignore from plugin config\n if (pluginConfig?.slugsToIgnore) {\n if (Array.isArray(pluginConfig.slugsToIgnore)) {\n slugsToIgnore.push(...pluginConfig.slugsToIgnore)\n } else {\n //Object with ignoreList and replaceDefaults\n if (pluginConfig.slugsToIgnore.replaceDefaults) {\n //Replace defaults\n slugsToIgnore = [] //Reset\n }\n slugsToIgnore.push(...pluginConfig.slugsToIgnore.ignoreList)\n }\n }\n\n if (config.collections) {\n config.collections.forEach((collection) => {\n if (slugsToIgnore.includes(collection.slug)) {\n return\n }\n\n const groupName = extractLocalizedGroupName(collection, currentLang) || 'Collections'\n // console.log(collection.slug, 'groupName:', groupName, 'Object', collection.admin)\n if (!avaibleGroups.has(groupName)) {\n avaibleGroups.add(groupName)\n groups.push({\n items: [],\n title: groupName,\n })\n }\n const group = groups.find((g) => g.title === groupName)\n if (group) {\n const useAsTitleField = collection.admin?.useAsTitle || 'id'\n let useAsTitleFieldLabel: TextFieldClient['label'] | undefined = undefined\n let useAsTitleLabel: string | undefined = undefined\n //Only extract useAsTitle label if submenu is enabled\n if (pluginConfig?.submenu?.enabled !== false) {\n useAsTitleFieldLabel = (\n collection?.fields?.find((field) => {\n if ('name' in field === false) {\n return null\n }\n return field.name === useAsTitleField\n }) as null | TextFieldClient\n )?.label\n //Extract label for useAsTitle field\n useAsTitleLabel = extractLocalizedValue(\n typeof useAsTitleFieldLabel === 'function' ? {} : useAsTitleFieldLabel || {},\n currentLang,\n useAsTitleField,\n )\n }\n group.items.push({\n slug: collection.slug,\n type: 'collection',\n action: {\n type: 'link',\n href: formatAdminURL({ adminRoute, path: `/collections/${collection.slug}` }),\n },\n //Either custom icon from plugin config, or default Files icon\n icon: pluginConfig?.icons?.collections?.[collection.slug] || Files,\n label: extractLocalizedCollectionName(collection, currentLang),\n useAsTitle: useAsTitleField,\n useAsTitleLabel: useAsTitleLabel || useAsTitleField,\n })\n }\n })\n }\n //Globals\n if (config.globals) {\n config.globals.forEach((global) => {\n if (slugsToIgnore.includes(global.slug)) {\n return\n }\n //Same logic as collections\n const groupName = extractLocalizedGroupName(global, currentLang) || 'Globals'\n if (!avaibleGroups.has(groupName)) {\n avaibleGroups.add(groupName)\n groups.push({\n items: [],\n title: groupName,\n })\n }\n const group = groups.find((g) => g.title === groupName)\n if (group) {\n group.items.push({\n slug: global.slug,\n type: 'global',\n action: {\n type: 'link',\n href: formatAdminURL({ adminRoute, path: `/globals/${global.slug}` }),\n },\n //Either custom icon from plugin config, or default Globe icon\n icon: pluginConfig?.icons?.globals?.[global.slug] || Globe,\n label: extractLocalizedGlobalName(global, currentLang),\n })\n }\n })\n }\n //Now custom items/groups from plugin config\n if (pluginConfig?.customItems) {\n //We are not using slugsToIgnore for custom items, as they are user-defined\n pluginConfig.customItems.forEach((value) => {\n if (value.type === 'group') {\n const convertedGroup = convertConfigGroup(value, currentLang)\n //Check if group already exists using our set\n if (!avaibleGroups.has(convertedGroup.title)) {\n avaibleGroups.add(convertedGroup.title)\n groups.push({\n collectionContext: convertedGroup.collectionContext,\n collectionSlugs: convertedGroup.collectionSlugs,\n items: [], //Don't add items yet, will do below\n title: convertedGroup.title,\n })\n }\n const group = groups.find((g) => g.title === convertedGroup.title)\n if (group) {\n //Append items to existing group, or if it was empty - add items\n group.items.push(...convertedGroup.items)\n }\n }\n if (value.type === 'item') {\n //Stray item, add to items array\n const convertedItem = convertConfigItem(value, currentLang)\n items.push(convertedItem)\n }\n })\n }\n return { groups, items }\n}\n"],"names":["Files","Globe","formatAdminURL","convertSlugToTitle","slug","replace","char","toUpperCase","extractLocalizedValue","value","locale","extractLocalizedCollectionName","collection","labels","plural","singular","extractLocalizedGlobalName","global","label","extractLocalizedGroupName","object","admin","group","trim","convertConfigItem","item","currentLang","type","action","collectionContext","collectionSlugs","icon","convertConfigGroup","items","map","title","DEFAULT_SLUGS_TO_IGNORE","createDefaultGroups","config","pluginConfig","groups","avaibleGroups","Set","slugsToIgnore","adminRoute","routes","Array","isArray","push","replaceDefaults","ignoreList","collections","forEach","includes","groupName","has","add","find","g","useAsTitleField","useAsTitle","useAsTitleFieldLabel","undefined","useAsTitleLabel","submenu","enabled","fields","field","name","href","path","icons","globals","customItems","convertedGroup","convertedItem"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Handler type for command menu function actions.
3
+ */
4
+ export type CommandMenuActionHandler = () => Promise<void> | void;
5
+ /**
6
+ * Register a handler function under the given key.
7
+ * Calling this with the same key again will overwrite the previous handler.
8
+ */
9
+ export declare const registerCommandMenuAction: (key: string, handler: CommandMenuActionHandler) => void;
10
+ /**
11
+ * Remove a previously registered handler.
12
+ */
13
+ export declare const unregisterCommandMenuAction: (key: string) => void;
14
+ /**
15
+ * Look up a registered handler by key. Returns `undefined` if not found.
16
+ * Used internally by the command menu to execute function actions.
17
+ */
18
+ export declare const getCommandMenuAction: (key: string) => CommandMenuActionHandler | undefined;
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+ /**
3
+ * Client-side registry for function-based command menu actions.
4
+ *
5
+ * Because the plugin config is serialized across the Next.js server→client
6
+ * boundary, JavaScript functions cannot be embedded directly in the config.
7
+ * Instead, register handlers here by a string key and reference that key in
8
+ * `action: { type: 'function', key: '...' }`.
9
+ *
10
+ * @example
11
+ * // payload.config.ts
12
+ * customItems: [{
13
+ * type: 'item',
14
+ * slug: 'save-doc',
15
+ * label: 'Save Document',
16
+ * icon: 'Save',
17
+ * collectionSlugs: ['posts'],
18
+ * action: { type: 'function', key: 'save-current-doc' },
19
+ * }]
20
+ *
21
+ * // Your client-side component / layout
22
+ * import { registerCommandMenuAction } from '@veiag/payload-cmdk/client'
23
+ * registerCommandMenuAction('save-current-doc', () => {
24
+ * document.querySelector('form')?.requestSubmit()
25
+ * })
26
+ */ const registry = new Map();
27
+ /**
28
+ * Register a handler function under the given key.
29
+ * Calling this with the same key again will overwrite the previous handler.
30
+ */ export const registerCommandMenuAction = (key, handler)=>{
31
+ registry.set(key, handler);
32
+ };
33
+ /**
34
+ * Remove a previously registered handler.
35
+ */ export const unregisterCommandMenuAction = (key)=>{
36
+ registry.delete(key);
37
+ };
38
+ /**
39
+ * Look up a registered handler by key. Returns `undefined` if not found.
40
+ * Used internally by the command menu to execute function actions.
41
+ */ export const getCommandMenuAction = (key)=>{
42
+ return registry.get(key);
43
+ };
44
+
45
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/registry.ts"],"sourcesContent":["'use client'\n\n/**\n * Handler type for command menu function actions.\n */\nexport type CommandMenuActionHandler = () => Promise<void> | void\n\n/**\n * Client-side registry for function-based command menu actions.\n *\n * Because the plugin config is serialized across the Next.js server→client\n * boundary, JavaScript functions cannot be embedded directly in the config.\n * Instead, register handlers here by a string key and reference that key in\n * `action: { type: 'function', key: '...' }`.\n *\n * @example\n * // payload.config.ts\n * customItems: [{\n * type: 'item',\n * slug: 'save-doc',\n * label: 'Save Document',\n * icon: 'Save',\n * collectionSlugs: ['posts'],\n * action: { type: 'function', key: 'save-current-doc' },\n * }]\n *\n * // Your client-side component / layout\n * import { registerCommandMenuAction } from '@veiag/payload-cmdk/client'\n * registerCommandMenuAction('save-current-doc', () => {\n * document.querySelector('form')?.requestSubmit()\n * })\n */\nconst registry = new Map<string, CommandMenuActionHandler>()\n\n/**\n * Register a handler function under the given key.\n * Calling this with the same key again will overwrite the previous handler.\n */\nexport const registerCommandMenuAction = (key: string, handler: CommandMenuActionHandler): void => {\n registry.set(key, handler)\n}\n\n/**\n * Remove a previously registered handler.\n */\nexport const unregisterCommandMenuAction = (key: string): void => {\n registry.delete(key)\n}\n\n/**\n * Look up a registered handler by key. Returns `undefined` if not found.\n * Used internally by the command menu to execute function actions.\n */\nexport const getCommandMenuAction = (key: string): CommandMenuActionHandler | undefined => {\n return registry.get(key)\n}\n"],"names":["registry","Map","registerCommandMenuAction","key","handler","set","unregisterCommandMenuAction","delete","getCommandMenuAction","get"],"mappings":"AAAA;AAOA;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,MAAMA,WAAW,IAAIC;AAErB;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,CAACC,KAAaC;IACrDJ,SAASK,GAAG,CAACF,KAAKC;AACpB,EAAC;AAED;;CAEC,GACD,OAAO,MAAME,8BAA8B,CAACH;IAC1CH,SAASO,MAAM,CAACJ;AAClB,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMK,uBAAuB,CAACL;IACnC,OAAOH,SAASS,GAAG,CAACN;AACtB,EAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veiag/payload-cmdk",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "A command menu plugin for Payload CMS to enhance navigation and accessibility within the admin panel.",
5
5
  "author": "VeiaG",
6
6
  "license": "MIT",
@@ -1,2 +0,0 @@
1
- import type { PayloadHandler } from 'payload';
2
- export declare const customEndpointHandler: PayloadHandler;
@@ -1,7 +0,0 @@
1
- export const customEndpointHandler = ()=>{
2
- return Response.json({
3
- message: 'Hello from custom endpoint'
4
- });
5
- };
6
-
7
- //# sourceMappingURL=customEndpointHandler.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/endpoints/customEndpointHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\n\nexport const customEndpointHandler: PayloadHandler = () => {\n return Response.json({ message: 'Hello from custom endpoint' })\n}\n"],"names":["customEndpointHandler","Response","json","message"],"mappings":"AAEA,OAAO,MAAMA,wBAAwC;IACnD,OAAOC,SAASC,IAAI,CAAC;QAAEC,SAAS;IAA6B;AAC/D,EAAC"}