@veiag/payload-cmdk 1.0.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.
Files changed (38) hide show
  1. package/README.md +594 -0
  2. package/dist/components/CommandMenuContext.d.ts +15 -0
  3. package/dist/components/CommandMenuContext.js +430 -0
  4. package/dist/components/CommandMenuContext.js.map +1 -0
  5. package/dist/components/SearchButton.d.ts +8 -0
  6. package/dist/components/SearchButton.js +106 -0
  7. package/dist/components/SearchButton.js.map +1 -0
  8. package/dist/components/SearchButton.scss +133 -0
  9. package/dist/components/cmdk/command.scss +334 -0
  10. package/dist/components/cmdk/index.d.ts +12 -0
  11. package/dist/components/cmdk/index.js +77 -0
  12. package/dist/components/cmdk/index.js.map +1 -0
  13. package/dist/components/modal.scss +94 -0
  14. package/dist/endpoints/customEndpointHandler.d.ts +2 -0
  15. package/dist/endpoints/customEndpointHandler.js +7 -0
  16. package/dist/endpoints/customEndpointHandler.js.map +1 -0
  17. package/dist/exports/client.d.ts +2 -0
  18. package/dist/exports/client.js +4 -0
  19. package/dist/exports/client.js.map +1 -0
  20. package/dist/exports/rsc.d.ts +0 -0
  21. package/dist/exports/rsc.js +2 -0
  22. package/dist/exports/rsc.js.map +1 -0
  23. package/dist/hooks/useMutationObserver.d.ts +1 -0
  24. package/dist/hooks/useMutationObserver.js +21 -0
  25. package/dist/hooks/useMutationObserver.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +74 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/translations/index.d.ts +32 -0
  30. package/dist/translations/index.js +38 -0
  31. package/dist/translations/index.js.map +1 -0
  32. package/dist/types.d.ts +223 -0
  33. package/dist/types.js +6 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/utils/index.d.ts +30 -0
  36. package/dist/utils/index.js +191 -0
  37. package/dist/utils/index.js.map +1 -0
  38. package/package.json +126 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/exports/rsc.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export declare const useMutationObserver: (ref: React.RefObject<HTMLElement | null>, callback: MutationCallback, options?: MutationObserverInit) => void;
@@ -0,0 +1,21 @@
1
+ import { useEffect } from 'react';
2
+ export const useMutationObserver = (ref, callback, options = {
3
+ attributes: true,
4
+ characterData: true,
5
+ childList: true,
6
+ subtree: true
7
+ })=>{
8
+ useEffect(()=>{
9
+ if (ref.current) {
10
+ const observer = new MutationObserver(callback);
11
+ observer.observe(ref.current, options);
12
+ return ()=>observer.disconnect();
13
+ }
14
+ }, [
15
+ ref,
16
+ callback,
17
+ options
18
+ ]);
19
+ };
20
+
21
+ //# sourceMappingURL=useMutationObserver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useMutationObserver.ts"],"sourcesContent":["import { useEffect } from 'react'\n\nexport const useMutationObserver = (\n ref: React.RefObject<HTMLElement | null>,\n callback: MutationCallback,\n options: MutationObserverInit = {\n attributes: true,\n characterData: true,\n childList: true,\n subtree: true,\n },\n) => {\n useEffect(() => {\n if (ref.current) {\n const observer = new MutationObserver(callback)\n observer.observe(ref.current, options)\n return () => observer.disconnect()\n }\n }, [ref, callback, options])\n}\n"],"names":["useEffect","useMutationObserver","ref","callback","options","attributes","characterData","childList","subtree","current","observer","MutationObserver","observe","disconnect"],"mappings":"AAAA,SAASA,SAAS,QAAQ,QAAO;AAEjC,OAAO,MAAMC,sBAAsB,CACjCC,KACAC,UACAC,UAAgC;IAC9BC,YAAY;IACZC,eAAe;IACfC,WAAW;IACXC,SAAS;AACX,CAAC;IAEDR,UAAU;QACR,IAAIE,IAAIO,OAAO,EAAE;YACf,MAAMC,WAAW,IAAIC,iBAAiBR;YACtCO,SAASE,OAAO,CAACV,IAAIO,OAAO,EAAEL;YAC9B,OAAO,IAAMM,SAASG,UAAU;QAClC;IACF,GAAG;QAACX;QAAKC;QAAUC;KAAQ;AAC7B,EAAC"}
@@ -0,0 +1,3 @@
1
+ import { type Config } from 'payload';
2
+ import type { PluginCommandMenuConfig } from './types';
3
+ export declare const payloadCmdk: (pluginOptions: PluginCommandMenuConfig) => (config: Config) => Config;
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ import { deepMerge } from 'payload';
2
+ import { commandPluginTranslations } from './translations';
3
+ export const payloadCmdk = (pluginOptions)=>(config)=>{
4
+ /**
5
+ * If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.
6
+ * If your plugin heavily modifies the database schema, you may want to remove this property.
7
+ */ if (pluginOptions.disabled) {
8
+ return config;
9
+ }
10
+ if (!config.admin) {
11
+ config.admin = {};
12
+ }
13
+ if (!config.admin.components) {
14
+ config.admin.components = {};
15
+ }
16
+ if (!config.admin.components.providers) {
17
+ config.admin.components.providers = [];
18
+ }
19
+ config.admin.components.providers.push({
20
+ clientProps: {
21
+ pluginConfig: pluginOptions
22
+ },
23
+ path: '@veiag/payload-cmdk/client#CommandMenuProvider'
24
+ });
25
+ if (pluginOptions.searchButton !== false) {
26
+ let searchButtonPosition = 'actions';
27
+ if (pluginOptions.searchButton && pluginOptions.searchButton.position) {
28
+ searchButtonPosition = pluginOptions.searchButton.position;
29
+ }
30
+ if (searchButtonPosition === 'nav') {
31
+ if (!config.admin.components.beforeNavLinks) {
32
+ config.admin.components.beforeNavLinks = [];
33
+ }
34
+ config.admin.components.beforeNavLinks = [
35
+ {
36
+ clientProps: {
37
+ position: pluginOptions.searchButton?.position || 'nav',
38
+ shortcut: pluginOptions.shortcut || [
39
+ 'meta+k',
40
+ 'ctrl+k'
41
+ ]
42
+ },
43
+ path: '@veiag/payload-cmdk/client#SearchButton'
44
+ },
45
+ ...config.admin.components.beforeNavLinks
46
+ ];
47
+ } else {
48
+ if (!config.admin.components.actions) {
49
+ config.admin.components.actions = [];
50
+ }
51
+ config.admin.components.actions.push({
52
+ clientProps: {
53
+ position: pluginOptions.searchButton?.position || 'actions',
54
+ shortcut: pluginOptions.shortcut || [
55
+ 'meta+k',
56
+ 'ctrl+k'
57
+ ]
58
+ },
59
+ path: '@veiag/payload-cmdk/client#SearchButton'
60
+ });
61
+ }
62
+ }
63
+ //Adding translations
64
+ if (!config.i18n) {
65
+ config.i18n = {};
66
+ }
67
+ if (!config.i18n.translations) {
68
+ config.i18n.translations = {};
69
+ }
70
+ config.i18n.translations = deepMerge(config.i18n.translations, commandPluginTranslations);
71
+ return config;
72
+ };
73
+
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { type Config, deepMerge } from 'payload'\n\nimport type { PluginCommandMenuConfig } from './types'\n\nimport { commandPluginTranslations } from './translations'\n\nexport const payloadCmdk =\n (pluginOptions: PluginCommandMenuConfig) =>\n (config: Config): Config => {\n /**\n * If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.\n * If your plugin heavily modifies the database schema, you may want to remove this property.\n */\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.admin) {\n config.admin = {}\n }\n\n if (!config.admin.components) {\n config.admin.components = {}\n }\n\n if (!config.admin.components.providers) {\n config.admin.components.providers = []\n }\n\n config.admin.components.providers.push({\n clientProps: {\n pluginConfig: pluginOptions,\n },\n path: '@veiag/payload-cmdk/client#CommandMenuProvider',\n })\n\n if (pluginOptions.searchButton !== false) {\n let searchButtonPosition = 'actions'\n if (pluginOptions.searchButton && pluginOptions.searchButton.position) {\n searchButtonPosition = pluginOptions.searchButton.position\n }\n if (searchButtonPosition === 'nav') {\n if (!config.admin.components.beforeNavLinks) {\n config.admin.components.beforeNavLinks = []\n }\n config.admin.components.beforeNavLinks = [\n {\n clientProps: {\n position: pluginOptions.searchButton?.position || 'nav',\n shortcut: pluginOptions.shortcut || ['meta+k', 'ctrl+k'],\n },\n path: '@veiag/payload-cmdk/client#SearchButton',\n },\n ...config.admin.components.beforeNavLinks,\n ]\n } else {\n if (!config.admin.components.actions) {\n config.admin.components.actions = []\n }\n config.admin.components.actions.push({\n clientProps: {\n position: pluginOptions.searchButton?.position || 'actions',\n shortcut: pluginOptions.shortcut || ['meta+k', 'ctrl+k'],\n },\n path: '@veiag/payload-cmdk/client#SearchButton',\n })\n }\n }\n\n //Adding translations\n if (!config.i18n) {\n config.i18n = {}\n }\n if (!config.i18n.translations) {\n config.i18n.translations = {}\n }\n\n config.i18n.translations = deepMerge(config.i18n.translations, commandPluginTranslations)\n\n return config\n }\n"],"names":["deepMerge","commandPluginTranslations","payloadCmdk","pluginOptions","config","disabled","admin","components","providers","push","clientProps","pluginConfig","path","searchButton","searchButtonPosition","position","beforeNavLinks","shortcut","actions","i18n","translations"],"mappings":"AAAA,SAAsBA,SAAS,QAAQ,UAAS;AAIhD,SAASC,yBAAyB,QAAQ,iBAAgB;AAE1D,OAAO,MAAMC,cACX,CAACC,gBACD,CAACC;QACC;;;KAGC,GACD,IAAID,cAAcE,QAAQ,EAAE;YAC1B,OAAOD;QACT;QAEA,IAAI,CAACA,OAAOE,KAAK,EAAE;YACjBF,OAAOE,KAAK,GAAG,CAAC;QAClB;QAEA,IAAI,CAACF,OAAOE,KAAK,CAACC,UAAU,EAAE;YAC5BH,OAAOE,KAAK,CAACC,UAAU,GAAG,CAAC;QAC7B;QAEA,IAAI,CAACH,OAAOE,KAAK,CAACC,UAAU,CAACC,SAAS,EAAE;YACtCJ,OAAOE,KAAK,CAACC,UAAU,CAACC,SAAS,GAAG,EAAE;QACxC;QAEAJ,OAAOE,KAAK,CAACC,UAAU,CAACC,SAAS,CAACC,IAAI,CAAC;YACrCC,aAAa;gBACXC,cAAcR;YAChB;YACAS,MAAM;QACR;QAEA,IAAIT,cAAcU,YAAY,KAAK,OAAO;YACxC,IAAIC,uBAAuB;YAC3B,IAAIX,cAAcU,YAAY,IAAIV,cAAcU,YAAY,CAACE,QAAQ,EAAE;gBACrED,uBAAuBX,cAAcU,YAAY,CAACE,QAAQ;YAC5D;YACA,IAAID,yBAAyB,OAAO;gBAClC,IAAI,CAACV,OAAOE,KAAK,CAACC,UAAU,CAACS,cAAc,EAAE;oBAC3CZ,OAAOE,KAAK,CAACC,UAAU,CAACS,cAAc,GAAG,EAAE;gBAC7C;gBACAZ,OAAOE,KAAK,CAACC,UAAU,CAACS,cAAc,GAAG;oBACvC;wBACEN,aAAa;4BACXK,UAAUZ,cAAcU,YAAY,EAAEE,YAAY;4BAClDE,UAAUd,cAAcc,QAAQ,IAAI;gCAAC;gCAAU;6BAAS;wBAC1D;wBACAL,MAAM;oBACR;uBACGR,OAAOE,KAAK,CAACC,UAAU,CAACS,cAAc;iBAC1C;YACH,OAAO;gBACL,IAAI,CAACZ,OAAOE,KAAK,CAACC,UAAU,CAACW,OAAO,EAAE;oBACpCd,OAAOE,KAAK,CAACC,UAAU,CAACW,OAAO,GAAG,EAAE;gBACtC;gBACAd,OAAOE,KAAK,CAACC,UAAU,CAACW,OAAO,CAACT,IAAI,CAAC;oBACnCC,aAAa;wBACXK,UAAUZ,cAAcU,YAAY,EAAEE,YAAY;wBAClDE,UAAUd,cAAcc,QAAQ,IAAI;4BAAC;4BAAU;yBAAS;oBAC1D;oBACAL,MAAM;gBACR;YACF;QACF;QAEA,qBAAqB;QACrB,IAAI,CAACR,OAAOe,IAAI,EAAE;YAChBf,OAAOe,IAAI,GAAG,CAAC;QACjB;QACA,IAAI,CAACf,OAAOe,IAAI,CAACC,YAAY,EAAE;YAC7BhB,OAAOe,IAAI,CAACC,YAAY,GAAG,CAAC;QAC9B;QAEAhB,OAAOe,IAAI,CAACC,YAAY,GAAGpB,UAAUI,OAAOe,IAAI,CAACC,YAAY,EAAEnB;QAE/D,OAAOG;IACT,EAAC"}
@@ -0,0 +1,32 @@
1
+ import type { NestedKeysStripped } from '@payloadcms/translations';
2
+ import type { enTranslations } from '@payloadcms/translations/languages/en';
3
+ export declare const commandPluginTranslations: {
4
+ en: {
5
+ cmdkPlugin: {
6
+ loading: string;
7
+ navigate: string;
8
+ noResults: string;
9
+ open: string;
10
+ search: string;
11
+ searchIn: string;
12
+ searchInCollection: string;
13
+ searchShort: string;
14
+ };
15
+ };
16
+ uk: {
17
+ cmdkPlugin: {
18
+ loading: string;
19
+ navigate: string;
20
+ noResults: string;
21
+ open: string;
22
+ search: string;
23
+ searchIn: string;
24
+ searchInCollection: string;
25
+ searchShort: string;
26
+ };
27
+ };
28
+ };
29
+ export type AvaibleTranslation = keyof typeof commandPluginTranslations.en.cmdkPlugin;
30
+ export type CustomTranslationsObject = typeof commandPluginTranslations.en & typeof enTranslations;
31
+ export type CustomTranslationsKeys = NestedKeysStripped<CustomTranslationsObject>;
32
+ export declare const mergeTranslations: (existingTranslations: Record<string, unknown>, newTranslations: Record<string, unknown>) => Record<string, unknown>;
@@ -0,0 +1,38 @@
1
+ export const commandPluginTranslations = {
2
+ en: {
3
+ cmdkPlugin: {
4
+ loading: 'Loading...',
5
+ navigate: 'to navigate',
6
+ noResults: 'No results found',
7
+ open: 'to open',
8
+ search: 'Search collections, globals...',
9
+ searchIn: 'Search in {{label}}',
10
+ searchInCollection: 'to search in collection',
11
+ searchShort: 'Search'
12
+ }
13
+ },
14
+ uk: {
15
+ cmdkPlugin: {
16
+ loading: 'Завантаження...',
17
+ navigate: 'щоб перейти',
18
+ noResults: 'Результатів не знайдено',
19
+ open: 'щоб відкрити',
20
+ search: 'Пошук колекцій, глобалів...',
21
+ searchIn: 'Пошук в {{label}}',
22
+ searchInCollection: 'щоб шукати в колекції',
23
+ searchShort: 'Пошук'
24
+ }
25
+ }
26
+ };
27
+ export const mergeTranslations = (existingTranslations, newTranslations)=>{
28
+ return {
29
+ ...existingTranslations,
30
+ ...newTranslations,
31
+ cmdkPlugin: {
32
+ ...existingTranslations.cmdkPlugin || {},
33
+ ...newTranslations.cmdkPlugin || {}
34
+ }
35
+ };
36
+ };
37
+
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import type { NestedKeysStripped } from '@payloadcms/translations'\nimport type { enTranslations } from '@payloadcms/translations/languages/en'\n\nexport const commandPluginTranslations = {\n en: {\n cmdkPlugin: {\n loading: 'Loading...',\n navigate: 'to navigate',\n noResults: 'No results found',\n open: 'to open',\n search: 'Search collections, globals...',\n searchIn: 'Search in {{label}}',\n searchInCollection: 'to search in collection',\n searchShort: 'Search',\n },\n },\n uk: {\n cmdkPlugin: {\n loading: 'Завантаження...',\n navigate: 'щоб перейти',\n noResults: 'Результатів не знайдено',\n open: 'щоб відкрити',\n search: 'Пошук колекцій, глобалів...',\n searchIn: 'Пошук в {{label}}',\n searchInCollection: 'щоб шукати в колекції',\n searchShort: 'Пошук',\n },\n },\n}\n\nexport type AvaibleTranslation = keyof typeof commandPluginTranslations.en.cmdkPlugin\n\nexport type CustomTranslationsObject = typeof commandPluginTranslations.en & typeof enTranslations\n\nexport type CustomTranslationsKeys = NestedKeysStripped<CustomTranslationsObject>\n\nexport const mergeTranslations = (\n existingTranslations: Record<string, unknown>,\n newTranslations: Record<string, unknown>,\n): Record<string, unknown> => {\n return {\n ...existingTranslations,\n ...newTranslations,\n cmdkPlugin: {\n ...(existingTranslations.cmdkPlugin || {}),\n ...(newTranslations.cmdkPlugin || {}),\n },\n }\n}\n"],"names":["commandPluginTranslations","en","cmdkPlugin","loading","navigate","noResults","open","search","searchIn","searchInCollection","searchShort","uk","mergeTranslations","existingTranslations","newTranslations"],"mappings":"AAGA,OAAO,MAAMA,4BAA4B;IACvCC,IAAI;QACFC,YAAY;YACVC,SAAS;YACTC,UAAU;YACVC,WAAW;YACXC,MAAM;YACNC,QAAQ;YACRC,UAAU;YACVC,oBAAoB;YACpBC,aAAa;QACf;IACF;IACAC,IAAI;QACFT,YAAY;YACVC,SAAS;YACTC,UAAU;YACVC,WAAW;YACXC,MAAM;YACNC,QAAQ;YACRC,UAAU;YACVC,oBAAoB;YACpBC,aAAa;QACf;IACF;AACF,EAAC;AAQD,OAAO,MAAME,oBAAoB,CAC/BC,sBACAC;IAEA,OAAO;QACL,GAAGD,oBAAoB;QACvB,GAAGC,eAAe;QAClBZ,YAAY;YACV,GAAIW,qBAAqBX,UAAU,IAAI,CAAC,CAAC;YACzC,GAAIY,gBAAgBZ,UAAU,IAAI,CAAC,CAAC;QACtC;IACF;AACF,EAAC"}
@@ -0,0 +1,223 @@
1
+ import type { LucideIcon } from 'lucide-react';
2
+ import type { IconName } from 'lucide-react/dynamic';
3
+ import type { CollectionSlug, GlobalSlug } from 'payload';
4
+ export type LocalizedString = {
5
+ [locale: string]: string;
6
+ } | string;
7
+ export type InternalIcon = IconName | LucideIcon;
8
+ /**
9
+ * Custom menu item, for configuration.
10
+ * Will be mapped to CommandMenuItem internally.
11
+ */
12
+ export type CustomMenuItem = {
13
+ action: CommandMenuAction;
14
+ icon?: IconName;
15
+ label: LocalizedString;
16
+ slug: string;
17
+ type: 'item';
18
+ };
19
+ /**
20
+ * Custom menu group, for configuration.
21
+ * Will be mapped to CommandMenuGroup internally.
22
+ *
23
+ * Groups will be merged if they have the same title.
24
+ */
25
+ export type CustomMenuGroup = {
26
+ items: CustomMenuItem[];
27
+ title: LocalizedString;
28
+ type: 'group';
29
+ };
30
+ /**
31
+ * Full serializable config for the plugin.
32
+ */
33
+ export type PluginCommandMenuConfig = {
34
+ /**
35
+ * Enable backdrop blur effect
36
+ * @default true
37
+ */
38
+ blurBg?: boolean;
39
+ /**
40
+ * Custom items or groups to add to the command menu.
41
+ */
42
+ customItems?: (CustomMenuGroup | CustomMenuItem)[];
43
+ /**
44
+ * Disable the plugin functionality
45
+ * @default false
46
+ */
47
+ disabled?: boolean;
48
+ /**
49
+ * Custom icons for collections and globals.
50
+ * Key is the collection slug, value is the icon name from lucide-react.
51
+ * Collections default icon - Files,
52
+ * Globals default icon - Globe.
53
+ */
54
+ icons?: {
55
+ /**
56
+ * Custom icons for collections.
57
+ * @default <Files/>
58
+ */
59
+ collections?: {
60
+ [key: CollectionSlug]: IconName;
61
+ };
62
+ /**
63
+ * Custom icons for globals.
64
+ * @default <Globe/>
65
+ */
66
+ globals?: {
67
+ [key: GlobalSlug]: IconName;
68
+ };
69
+ };
70
+ /**
71
+ * Configuration for the search button in the admin navigation.
72
+ * Set to false to disable the search button.
73
+ * @default { position: 'actions' }
74
+ */
75
+ searchButton?: {
76
+ /**
77
+ * Position of the search button in the admin navigation.
78
+ * @default 'actions'
79
+ */
80
+ position?: 'actions' | 'nav';
81
+ } | false;
82
+ /**
83
+ * Keyboard shortcut to open the command menu.
84
+ * Can be a single shortcut string or an array of shortcuts for cross-platform support.
85
+ * @default ['meta+k', 'ctrl+k']
86
+ * @example 'mod+k' or ['meta+k', 'ctrl+k']
87
+ *
88
+ * More details here - https://react-hotkeys-hook.vercel.app/docs/intro
89
+ */
90
+ shortcut?: string | string[];
91
+ /**
92
+ * Specify which collections slugs remove from the command menu.
93
+ * @default ['payload-migrations','payload-preferences','payload-locked-documents']
94
+ *
95
+ * You can also provide an object with `ignoreList` and `replaceDefaults` properties.
96
+ * `replaceDefaults` allows you to completely replace the default slugs to ignore instead of appending to them.
97
+ */
98
+ slugsToIgnore?: {
99
+ /**
100
+ * List of collection/global slugs to ignore in the command menu.
101
+ */
102
+ ignoreList: CollectionSlug[];
103
+ /**
104
+ * Whether to replace the default slugs to ignore instead of appending to them.
105
+ */
106
+ replaceDefaults?: boolean;
107
+ } | CollectionSlug[];
108
+ /**
109
+ * Configure submenu behavior for collections.
110
+ * When enabled, users can search within a collection's documents.
111
+ * @default { enabled: true, shortcut: 'shift+enter' }
112
+ */
113
+ submenu?: {
114
+ /**
115
+ * Enable or disable submenu functionality.
116
+ * @default true
117
+ */
118
+ enabled?: boolean;
119
+ /**
120
+ * Custom icons for collection submenus.
121
+ * Key is the collection slug, value is the icon name from lucide-react.
122
+ *
123
+ * @default null
124
+ */
125
+ icons?: {
126
+ [key: CollectionSlug]: IconName;
127
+ };
128
+ /**
129
+ * Keyboard shortcut to open collection submenu.
130
+ * - 'shift+enter': Shift+Enter opens submenu, Enter navigates to collection list
131
+ * - 'enter': Enter opens submenu, Shift+Enter navigates to collection list
132
+ * @default 'shift+enter'
133
+ */
134
+ shortcut?: 'enter' | 'shift+enter';
135
+ };
136
+ };
137
+ export interface CommandMenuContextProps {
138
+ children: React.ReactNode;
139
+ pluginConfig: PluginCommandMenuConfig;
140
+ }
141
+ export interface CommandMenuActionLink {
142
+ href: string;
143
+ type: 'link';
144
+ }
145
+ export interface CommandMenuActionAPICall {
146
+ body?: {
147
+ [key: string]: unknown;
148
+ };
149
+ href: string;
150
+ /**
151
+ * HTTP method to use for the API call.
152
+ * @default 'GET'
153
+ */
154
+ method?: 'DELETE' | 'GET' | 'POST' | 'PUT';
155
+ type: 'api';
156
+ }
157
+ export type CommandMenuAction = CommandMenuActionAPICall | CommandMenuActionLink;
158
+ export interface CommandMenuItem {
159
+ /**
160
+ * Action to perform when the command menu item is selected.
161
+ */
162
+ action: CommandMenuAction;
163
+ icon?: InternalIcon;
164
+ label: string;
165
+ slug: string;
166
+ /**
167
+ * Type of the command menu item. Used for grouping and icons.
168
+ * @default 'custom'
169
+ */
170
+ type: 'collection' | 'custom' | 'global';
171
+ /**
172
+ * Field name used as title for collection documents.
173
+ * Only applicable for collection type items.
174
+ * Defaults to 'id' if not specified.
175
+ */
176
+ useAsTitle?: string;
177
+ /**
178
+ * Label for the field used as title for collection documents.
179
+ * Only applicable for collection type items.
180
+ *
181
+ * Used in submenu search placeholder.
182
+ */
183
+ useAsTitleLabel?: string;
184
+ }
185
+ export interface CommandMenuGroup {
186
+ items: CommandMenuItem[];
187
+ title: string;
188
+ }
189
+ /**
190
+ * Page state for command menu navigation.
191
+ * - 'main': Default view showing all collections/globals/custom items
192
+ * - CollectionSearchPage: Submenu view for searching within a specific collection
193
+ */
194
+ export type CommandMenuPage = 'main' | {
195
+ /**
196
+ * Collection label for display
197
+ */
198
+ label: string;
199
+ /**
200
+ * Collection slug
201
+ */
202
+ slug: string;
203
+ /**
204
+ * Page type identifier
205
+ */
206
+ type: 'collection-search';
207
+ /**
208
+ * Field name to use as document title
209
+ */
210
+ useAsTitle: string;
211
+ /**
212
+ * Label for the field used as title
213
+ */
214
+ useAsTitleLabel: string;
215
+ };
216
+ /**
217
+ * Generic document type for collections, with dynamic keys.
218
+ * We assume values are either string or number for simplicity, useAsTitle is making sure of that.
219
+ */
220
+ export type GenericCollectionDocument = {
221
+ [key: string]: number | string;
222
+ id: string;
223
+ };
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generic document type for collections, with dynamic keys.
3
+ * We assume values are either string or number for simplicity, useAsTitle is making sure of that.
4
+ */ export { };
5
+
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { LucideIcon } from 'lucide-react'\nimport type { IconName } from 'lucide-react/dynamic'\nimport type { CollectionSlug, GlobalSlug } from 'payload'\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?: {\n [key: CollectionSlug]: IconName\n }\n /**\n * Custom icons for globals.\n * @default <Globe/>\n */\n globals?: {\n [key: GlobalSlug]: IconName\n }\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?: {\n [key: CollectionSlug]: IconName\n }\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":"AAwOA;;;CAGC,GACD,WAGC"}
@@ -0,0 +1,30 @@
1
+ import type { ClientConfig, LabelFunction } from 'payload';
2
+ import type { CommandMenuGroup, CommandMenuItem, CustomMenuGroup, CustomMenuItem, LocalizedString, PluginCommandMenuConfig } from 'src/types';
3
+ export declare const convertSlugToTitle: (slug: string) => string;
4
+ export declare const extractLocalizedValue: (value: LocalizedString, locale: string, slug?: string) => string;
5
+ export declare const extractLocalizedCollectionName: (collection: {
6
+ labels?: {
7
+ plural?: LocalizedString;
8
+ singular?: LocalizedString;
9
+ };
10
+ slug: string;
11
+ }, locale: string) => string;
12
+ export declare const extractLocalizedGlobalName: (global: {
13
+ label?: LabelFunction | LocalizedString;
14
+ slug: string;
15
+ }, locale: string) => string;
16
+ export declare const extractLocalizedGroupName: (object: {
17
+ admin?: {
18
+ group?: false | LocalizedString;
19
+ };
20
+ }, locale: string) => null | string;
21
+ export declare const convertConfigItem: (item: CustomMenuItem, currentLang: string) => CommandMenuItem;
22
+ export declare const convertConfigGroup: (group: CustomMenuGroup, currentLang: string) => CommandMenuGroup;
23
+ export declare const createDefaultGroups: (config: ClientConfig, currentLang: string, pluginConfig: PluginCommandMenuConfig) => {
24
+ groups: CommandMenuGroup[];
25
+ /**
26
+ * Stray items that don't belong to any group.
27
+ * Only with custom items.
28
+ */
29
+ items: CommandMenuItem[];
30
+ };
@@ -0,0 +1,191 @@
1
+ import { Files, Globe } from 'lucide-react';
2
+ export const convertSlugToTitle = (slug)=>{
3
+ return slug.replace(/-/g, ' ').replace(/\b\w/g, (char)=>char.toUpperCase());
4
+ };
5
+ export const extractLocalizedValue = (value, locale, slug)=>{
6
+ if (typeof value === 'string') {
7
+ return value;
8
+ }
9
+ return value[locale] || convertSlugToTitle(slug || '');
10
+ };
11
+ export const extractLocalizedCollectionName = (collection, locale)=>{
12
+ //Get plural name if exists, otherwise singular, otherwise slug
13
+ if (collection.labels?.plural) {
14
+ return extractLocalizedValue(collection.labels.plural, locale, collection.slug);
15
+ }
16
+ if (collection.labels?.singular) {
17
+ return extractLocalizedValue(collection.labels.singular, locale, collection.slug);
18
+ }
19
+ return '' //Generally should not happen
20
+ ;
21
+ };
22
+ export const extractLocalizedGlobalName = (global, locale)=>{
23
+ if (global.label) {
24
+ return extractLocalizedValue(//Ignore label functions, they are not serializable
25
+ typeof global.label === 'function' ? {} : global.label, locale, global.slug);
26
+ }
27
+ return '' //Generally should not happen
28
+ ;
29
+ };
30
+ export const extractLocalizedGroupName = (object, locale)=>{
31
+ if (object.admin?.group) {
32
+ //Try to extract group name, with fallback to null (no group)
33
+ return extractLocalizedValue(object.admin.group, locale)?.trim() || null;
34
+ }
35
+ return null;
36
+ };
37
+ export const convertConfigItem = (item, currentLang)=>{
38
+ return {
39
+ slug: item.slug,
40
+ type: 'custom',
41
+ action: item.action,
42
+ icon: item.icon,
43
+ label: extractLocalizedValue(item.label, currentLang, item.slug)
44
+ };
45
+ };
46
+ export const convertConfigGroup = (group, currentLang)=>{
47
+ return {
48
+ items: group.items.map((item)=>convertConfigItem(item, currentLang)),
49
+ title: extractLocalizedValue(group.title, currentLang)
50
+ };
51
+ };
52
+ /**
53
+ * Set of collection/globals slugs to ignore in the command menu.
54
+ * This is useful to avoid showing certain collections/globals in the command menu.
55
+ */ const DEFAULT_SLUGS_TO_IGNORE = [
56
+ 'payload-migrations',
57
+ 'payload-preferences',
58
+ 'payload-locked-documents'
59
+ ];
60
+ export const createDefaultGroups = (config, currentLang, pluginConfig)=>{
61
+ const groups = [];
62
+ const items = [];
63
+ const avaibleGroups = new Set() //To avoid duplicates
64
+ ;
65
+ let slugsToIgnore = [
66
+ ...DEFAULT_SLUGS_TO_IGNORE
67
+ ];
68
+ //Handle slugs to ignore from plugin config
69
+ if (pluginConfig?.slugsToIgnore) {
70
+ if (Array.isArray(pluginConfig.slugsToIgnore)) {
71
+ slugsToIgnore.push(...pluginConfig.slugsToIgnore);
72
+ } else {
73
+ //Object with ignoreList and replaceDefaults
74
+ if (pluginConfig.slugsToIgnore.replaceDefaults) {
75
+ //Replace defaults
76
+ slugsToIgnore = []; //Reset
77
+ }
78
+ slugsToIgnore.push(...pluginConfig.slugsToIgnore.ignoreList);
79
+ }
80
+ }
81
+ if (config.collections) {
82
+ config.collections.forEach((collection)=>{
83
+ if (slugsToIgnore.includes(collection.slug)) {
84
+ return;
85
+ }
86
+ const groupName = extractLocalizedGroupName(collection, currentLang) || 'Collections';
87
+ // console.log(collection.slug, 'groupName:', groupName, 'Object', collection.admin)
88
+ if (!avaibleGroups.has(groupName)) {
89
+ avaibleGroups.add(groupName);
90
+ groups.push({
91
+ items: [],
92
+ title: groupName
93
+ });
94
+ }
95
+ const group = groups.find((g)=>g.title === groupName);
96
+ if (group) {
97
+ const useAsTitleField = collection.admin?.useAsTitle || 'id';
98
+ let useAsTitleFieldLabel = undefined;
99
+ let useAsTitleLabel = undefined;
100
+ //Only extract useAsTitle label if submenu is enabled
101
+ if (pluginConfig?.submenu?.enabled !== false) {
102
+ useAsTitleFieldLabel = collection?.fields?.find((field)=>{
103
+ if ('name' in field === false) {
104
+ return null;
105
+ }
106
+ return field.name === useAsTitleField;
107
+ })?.label;
108
+ //Extract label for useAsTitle field
109
+ useAsTitleLabel = extractLocalizedValue(typeof useAsTitleFieldLabel === 'function' ? {} : useAsTitleFieldLabel || {}, currentLang, useAsTitleField);
110
+ }
111
+ group.items.push({
112
+ slug: collection.slug,
113
+ type: 'collection',
114
+ action: {
115
+ type: 'link',
116
+ href: `/admin/collections/${collection.slug}`
117
+ },
118
+ //Either custom icon from plugin config, or default Files icon
119
+ icon: pluginConfig?.icons?.collections?.[collection.slug] || Files,
120
+ label: extractLocalizedCollectionName(collection, currentLang),
121
+ useAsTitle: useAsTitleField,
122
+ useAsTitleLabel: useAsTitleLabel || useAsTitleField
123
+ });
124
+ }
125
+ });
126
+ }
127
+ //Globals
128
+ if (config.globals) {
129
+ config.globals.forEach((global)=>{
130
+ if (slugsToIgnore.includes(global.slug)) {
131
+ return;
132
+ }
133
+ //Same logic as collections
134
+ const groupName = extractLocalizedGroupName(global, currentLang) || 'Globals';
135
+ if (!avaibleGroups.has(groupName)) {
136
+ avaibleGroups.add(groupName);
137
+ groups.push({
138
+ items: [],
139
+ title: groupName
140
+ });
141
+ }
142
+ const group = groups.find((g)=>g.title === groupName);
143
+ if (group) {
144
+ group.items.push({
145
+ slug: global.slug,
146
+ type: 'global',
147
+ action: {
148
+ type: 'link',
149
+ href: `/admin/globals/${global.slug}`
150
+ },
151
+ //Either custom icon from plugin config, or default Globe icon
152
+ icon: pluginConfig?.icons?.globals?.[global.slug] || Globe,
153
+ label: extractLocalizedGlobalName(global, currentLang)
154
+ });
155
+ }
156
+ });
157
+ }
158
+ //Now custom items/groups from plugin config
159
+ if (pluginConfig?.customItems) {
160
+ //We are not using slugsToIgnore for custom items, as they are user-defined
161
+ pluginConfig.customItems.forEach((value)=>{
162
+ if (value.type === 'group') {
163
+ const convertedGroup = convertConfigGroup(value, currentLang);
164
+ //Check if group already exists using our set
165
+ if (!avaibleGroups.has(convertedGroup.title)) {
166
+ avaibleGroups.add(convertedGroup.title);
167
+ groups.push({
168
+ items: [],
169
+ title: convertedGroup.title
170
+ });
171
+ }
172
+ const group = groups.find((g)=>g.title === convertedGroup.title);
173
+ if (group) {
174
+ //Append items to existing group, or if it was empty - add items
175
+ group.items.push(...convertedGroup.items);
176
+ }
177
+ }
178
+ if (value.type === 'item') {
179
+ //Stray item, add to items array
180
+ const convertedItem = convertConfigItem(value, currentLang);
181
+ items.push(convertedItem);
182
+ }
183
+ });
184
+ }
185
+ return {
186
+ groups,
187
+ items
188
+ };
189
+ };
190
+
191
+ //# sourceMappingURL=index.js.map