@veiag/payload-enhanced-sidebar 0.3.0-beta.0 → 0.3.0-beta.2

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
@@ -380,6 +380,71 @@ Available colors: `default`, `primary`, `success`, `warning`, `error`
380
380
  - Zero or undefined values hide the badge
381
381
  - Provider values can also be React nodes for custom rendering
382
382
 
383
+ ## Access Control
384
+
385
+ You can control visibility of tabs, links, and custom items using an `access` function. It runs **server-side** and receives the current `PayloadRequest`, so you have full access to `req.user`, roles, permissions, etc.
386
+
387
+ ### On tabs and links
388
+
389
+ ```typescript
390
+ tabs: [
391
+ {
392
+ id: 'admin-panel',
393
+ type: 'tab',
394
+ icon: 'Shield',
395
+ label: 'Admin',
396
+ collections: ['users', 'tenants'],
397
+ access: ({ req, item }) => {
398
+ return req.user?.role === 'admin'
399
+ },
400
+ },
401
+ {
402
+ id: 'reports',
403
+ type: 'link',
404
+ href: '/reports',
405
+ icon: 'BarChart',
406
+ label: 'Reports',
407
+ access: async ({ req }) => {
408
+ // async is supported
409
+ return Boolean(req.user)
410
+ },
411
+ },
412
+ ]
413
+ ```
414
+
415
+ If `access` returns `false`, the tab button is hidden from the tabs bar and its content is not rendered.
416
+
417
+ ### On custom items
418
+
419
+ ```typescript
420
+ customItems: [
421
+ {
422
+ slug: 'admin-tools',
423
+ href: '/admin-tools',
424
+ label: 'Admin Tools',
425
+ access: ({ req }) => req.user?.role === 'admin',
426
+ },
427
+ ]
428
+ ```
429
+
430
+ **Access function signatures:**
431
+
432
+ ```typescript
433
+ // For tabs and links
434
+ type TabAccessFunction = (args: {
435
+ item: SidebarTab // full tab/link config
436
+ req: PayloadRequest
437
+ }) => boolean | Promise<boolean>
438
+
439
+ // For custom items
440
+ type ItemAccessFunction = (args: {
441
+ item: SidebarTabItem // full custom item config
442
+ req: PayloadRequest
443
+ }) => boolean | Promise<boolean>
444
+ ```
445
+
446
+ > Default collections and globals already respect Payload's built-in access control — they are filtered by `visibleEntities` automatically. The `access` function is only needed for tabs, links, and custom items.
447
+
383
448
  ### `showLogout`
384
449
 
385
450
  Show/hide the logout button at the bottom of the tabs bar.
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { getTranslation } from '@payloadcms/translations';
3
- import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
4
3
  import { NavGroup } from '@payloadcms/ui';
4
+ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
5
5
  import { EntityType, groupNavItems } from '@payloadcms/ui/shared';
6
6
  import { cookies } from 'next/headers';
7
7
  import { formatAdminURL } from 'payload/shared';
8
8
  import React, { Fragment } from 'react';
9
9
  const COOKIE_KEY = 'payload-enhanced-sidebar-active-tab';
10
- import { extractLocalizedValue, resolveSidebarComponent } from '../../utils';
10
+ import { extractLocalizedValue, resolveSidebarComponent, sanitizeSidebarConfig } from '../../utils';
11
11
  import { getNavPrefs } from './getNavPrefs';
12
12
  import { Icon } from './Icon';
13
13
  import { NavItem } from './NavItem';
@@ -15,7 +15,7 @@ import { SidebarContent } from './SidebarContent';
15
15
  import './index.scss';
16
16
  /**
17
17
  * Computes filtered and merged groups for a specific tab (server-side).
18
- */ const computeGroupsForTab = (tab, groups, currentLang)=>{
18
+ */ const computeGroupsForTab = async (tab, groups, currentLang, req)=>{
19
19
  const { collections: tabCollections, customItems, globals: tabGlobals } = tab;
20
20
  const showAll = !tabCollections && !tabGlobals;
21
21
  const allowedSlugs = new Set([
@@ -47,7 +47,13 @@ import './index.scss';
47
47
  isExternal: item.isExternal,
48
48
  label: item.label
49
49
  });
50
- for (const item of customItems){
50
+ // Filter custom items by access — fail-closed: missing req denies access
51
+ const accessResults = await Promise.all(customItems.map((item)=>item.access ? req ? item.access({
52
+ item,
53
+ req
54
+ }) : false : true));
55
+ const visibleItems = customItems.filter((_, i)=>accessResults[i]);
56
+ for (const item of visibleItems){
51
57
  if (item.group) {
52
58
  const itemGroupLabel = extractLocalizedValue(item.group, currentLang);
53
59
  const existingGroup = result.find((g)=>{
@@ -99,7 +105,7 @@ import './index.scss';
99
105
  return result;
100
106
  };
101
107
  export const EnhancedSidebar = async (props)=>{
102
- const { i18n, locale, params, payload, permissions, req, searchParams, sidebarConfig, user, visibleEntities, documentSubViewType, viewType } = props;
108
+ const { documentSubViewType, i18n, locale, params, payload, permissions, req, searchParams, sidebarConfig, user, viewType, visibleEntities } = props;
103
109
  if (!payload?.config) {
104
110
  return null;
105
111
  }
@@ -129,23 +135,23 @@ export const EnhancedSidebar = async (props)=>{
129
135
  viewType
130
136
  };
131
137
  const beforeNavLinksRendered = RenderServerComponent({
138
+ clientProps,
132
139
  Component: beforeNavLinks,
133
140
  importMap: payload.importMap,
134
- serverProps,
135
- clientProps
141
+ serverProps
136
142
  });
137
143
  const afterNavLinksRendered = RenderServerComponent({
144
+ clientProps,
138
145
  Component: afterNavLinks,
139
146
  importMap: payload.importMap,
140
- serverProps,
141
- clientProps
147
+ serverProps
142
148
  });
143
149
  const renderedSettingsMenu = settingsMenu && Array.isArray(settingsMenu) ? settingsMenu.map((item, index)=>RenderServerComponent({
150
+ clientProps,
144
151
  Component: item,
145
152
  importMap: payload.importMap,
146
153
  key: `settings-menu-item-${index}`,
147
- serverProps,
148
- clientProps
154
+ serverProps
149
155
  })) : [];
150
156
  const config = sidebarConfig ?? {
151
157
  tabs: [
@@ -157,10 +163,18 @@ export const EnhancedSidebar = async (props)=>{
157
163
  }
158
164
  ]
159
165
  };
166
+ // Filter all tab bar items by access — fail-closed: missing req denies access
167
+ // Note: req can be undefined on 404 pages due to a Payload bug (NotFound view omits req from props)
168
+ const allConfigTabs = config.tabs ?? [];
169
+ const tabAccessResults = await Promise.all(allConfigTabs.map((t)=>t.access ? req ? t.access({
170
+ item: t,
171
+ req
172
+ }) : false : true));
173
+ const visibleTabItems = allConfigTabs.filter((_, i)=>tabAccessResults[i]);
160
174
  // Read active tab from cookie
161
175
  const cookieStore = await cookies();
162
176
  const storedTabId = cookieStore.get(COOKIE_KEY)?.value;
163
- const tabs = config.tabs?.filter((t)=>t.type === 'tab') ?? [];
177
+ const tabs = visibleTabItems.filter((t)=>t.type === 'tab');
164
178
  const defaultTabId = tabs[0]?.id ?? 'default';
165
179
  const initialActiveTabId = storedTabId && tabs.some((t)=>t.id === storedTabId) ? storedTabId : defaultTabId;
166
180
  const adminRoute = payload.config.routes.admin;
@@ -195,19 +209,20 @@ export const EnhancedSidebar = async (props)=>{
195
209
  const badgeConfig = config.badges?.[slug];
196
210
  if (config.customComponents?.NavItem) {
197
211
  const label = getTranslation(entity.label, i18n);
198
- const { path, clientProps: extraProps } = resolveSidebarComponent(config.customComponents.NavItem);
212
+ const { clientProps: extraProps, path } = resolveSidebarComponent(config.customComponents.NavItem);
199
213
  return RenderServerComponent({
200
- Component: path,
201
- importMap: payload.importMap,
202
- key,
203
214
  clientProps: {
204
- entity,
205
- href,
206
215
  id,
207
216
  badgeConfig,
217
+ entity,
218
+ href,
208
219
  label,
209
220
  ...extraProps
210
- }
221
+ },
222
+ Component: path,
223
+ importMap: payload.importMap,
224
+ key,
225
+ serverProps
211
226
  });
212
227
  }
213
228
  return /*#__PURE__*/ _jsx(NavItem, {
@@ -230,17 +245,18 @@ export const EnhancedSidebar = async (props)=>{
230
245
  }, key);
231
246
  }
232
247
  if (config.customComponents?.NavGroup) {
233
- const { path, clientProps: extraProps } = resolveSidebarComponent(config.customComponents.NavGroup);
248
+ const { clientProps: extraProps, path } = resolveSidebarComponent(config.customComponents.NavGroup);
234
249
  return RenderServerComponent({
235
- Component: path,
236
- importMap: payload.importMap,
237
- key,
238
250
  clientProps: {
239
- label: translatedLabel,
240
- isOpen: navPreferences?.groups?.[translatedLabel]?.open,
241
251
  children: items,
252
+ isOpen: navPreferences?.groups?.[translatedLabel]?.open,
253
+ label: translatedLabel,
242
254
  ...extraProps
243
- }
255
+ },
256
+ Component: path,
257
+ importMap: payload.importMap,
258
+ key,
259
+ serverProps
244
260
  });
245
261
  }
246
262
  return /*#__PURE__*/ _jsx(NavGroup, {
@@ -249,12 +265,14 @@ export const EnhancedSidebar = async (props)=>{
249
265
  children: items
250
266
  }, key);
251
267
  };
252
- // Pre-render content for every tab on the server
268
+ // Pre-render content for every tab on the server (computed in parallel)
269
+ const tabGroupResults = await Promise.all(tabs.map((tab)=>computeGroupsForTab(tab, groups, currentLang, req)));
253
270
  const tabsContent = {};
254
- for (const tab of tabs){
255
- const tabGroups = computeGroupsForTab(tab, groups, currentLang);
271
+ for(let i = 0; i < tabs.length; i++){
272
+ const tab = tabs[i];
273
+ const tabGroups = tabGroupResults[i];
256
274
  tabsContent[tab.id] = /*#__PURE__*/ _jsx(Fragment, {
257
- children: tabGroups.map((group, i)=>renderGroup(group, `${tab.id}-${i}`))
275
+ children: tabGroups.map((group, j)=>renderGroup(group, `${tab.id}-${j}`))
258
276
  });
259
277
  }
260
278
  // For the no-tabs fallback
@@ -262,7 +280,7 @@ export const EnhancedSidebar = async (props)=>{
262
280
  children: groups.map((group, i)=>renderGroup(group, `all-${i}`))
263
281
  }) : undefined;
264
282
  // Build server-side icon and tab button rendering
265
- const allTabItems = config.tabs ?? [];
283
+ const allTabItems = visibleTabItems;
266
284
  const hasCustomTabButton = !!config.customComponents?.TabButton;
267
285
  const hasAnyIconComponent = allTabItems.some((t)=>t.iconComponent);
268
286
  // tabIcons: per-id icon node (only built when no custom TabButton, just iconComponent overrides)
@@ -275,22 +293,23 @@ export const EnhancedSidebar = async (props)=>{
275
293
  // Resolve icon: custom iconComponent > default Lucide
276
294
  let iconNode;
277
295
  if (item.iconComponent) {
278
- const { path: iconPath, clientProps: iconExtraProps } = resolveSidebarComponent(item.iconComponent);
296
+ const { clientProps: iconExtraProps, path: iconPath } = resolveSidebarComponent(item.iconComponent);
279
297
  iconNode = RenderServerComponent({
280
- Component: iconPath,
281
- importMap: payload.importMap,
282
298
  clientProps: {
283
299
  id: item.id,
284
- label,
285
300
  type: item.type,
301
+ label,
286
302
  ...iconExtraProps
287
- }
303
+ },
304
+ Component: iconPath,
305
+ importMap: payload.importMap,
306
+ serverProps
288
307
  });
289
308
  } else {
290
- iconNode = /*#__PURE__*/ _jsx(Icon, {
309
+ iconNode = item.icon ? /*#__PURE__*/ _jsx(Icon, {
291
310
  name: item.icon,
292
311
  size: 20
293
- });
312
+ }) : null;
294
313
  }
295
314
  if (hasCustomTabButton) {
296
315
  // Compute href for links
@@ -301,21 +320,22 @@ export const EnhancedSidebar = async (props)=>{
301
320
  path: item.href
302
321
  });
303
322
  }
304
- const { path: tabBtnPath, clientProps: tabBtnExtraProps } = resolveSidebarComponent(config.customComponents.TabButton);
323
+ const { clientProps: tabBtnExtraProps, path: tabBtnPath } = resolveSidebarComponent(config.customComponents.TabButton);
305
324
  renderedTabItems.push(RenderServerComponent({
306
- Component: tabBtnPath,
307
- importMap: payload.importMap,
308
- key: item.id,
309
325
  clientProps: {
326
+ id: item.id,
327
+ type: item.type,
310
328
  badge: item.badge,
311
329
  href,
312
330
  icon: iconNode,
313
- id: item.id,
314
331
  isExternal: item.type === 'link' ? item.isExternal : undefined,
315
332
  label,
316
- type: item.type,
317
333
  ...tabBtnExtraProps
318
- }
334
+ },
335
+ Component: tabBtnPath,
336
+ importMap: payload.importMap,
337
+ key: item.id,
338
+ serverProps
319
339
  }));
320
340
  } else if (item.iconComponent) {
321
341
  tabIcons[item.id] = iconNode;
@@ -323,11 +343,8 @@ export const EnhancedSidebar = async (props)=>{
323
343
  }
324
344
  }
325
345
  const customNavContent = config.customComponents?.NavContent ? (()=>{
326
- const { path, clientProps: extraProps } = resolveSidebarComponent(config.customComponents.NavContent);
346
+ const { clientProps: extraProps, path } = resolveSidebarComponent(config.customComponents.NavContent);
327
347
  return RenderServerComponent({
328
- Component: path,
329
- importMap: payload.importMap,
330
- key: 'enhanced-sidebar-nav-content',
331
348
  clientProps: {
332
349
  afterNavLinks: afterNavLinksRendered,
333
350
  allContent,
@@ -337,9 +354,18 @@ export const EnhancedSidebar = async (props)=>{
337
354
  })),
338
355
  tabsContent,
339
356
  ...extraProps
340
- }
357
+ },
358
+ Component: path,
359
+ importMap: payload.importMap,
360
+ key: 'enhanced-sidebar-nav-content',
361
+ serverProps
341
362
  });
342
363
  })() : undefined;
364
+ // Strip access functions and use only visible tabs before passing to client component
365
+ const clientSidebarConfig = sanitizeSidebarConfig({
366
+ ...config,
367
+ tabs: visibleTabItems
368
+ });
343
369
  return /*#__PURE__*/ _jsx(SidebarContent, {
344
370
  afterNavLinks: afterNavLinksRendered,
345
371
  allContent: allContent,
@@ -348,7 +374,7 @@ export const EnhancedSidebar = async (props)=>{
348
374
  initialActiveTabId: initialActiveTabId,
349
375
  renderedTabItems: renderedTabItems.length > 0 ? renderedTabItems : undefined,
350
376
  settingsMenu: renderedSettingsMenu,
351
- sidebarConfig: config,
377
+ sidebarConfig: clientSidebarConfig,
352
378
  tabIcons: Object.keys(tabIcons).length > 0 ? tabIcons : undefined,
353
379
  tabsContent: tabsContent
354
380
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/EnhancedSidebar/index.tsx"],"sourcesContent":["import type { EntityToGroup } from '@payloadcms/ui/shared'\nimport type { PayloadRequest, ServerProps } from 'payload'\n\nimport { getTranslation } from '@payloadcms/translations'\nimport { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'\nimport { NavGroup } from '@payloadcms/ui'\nimport { EntityType, groupNavItems } from '@payloadcms/ui/shared'\nimport { cookies } from 'next/headers'\nimport { formatAdminURL } from 'payload/shared'\nimport React, { Fragment } from 'react'\n\nconst COOKIE_KEY = 'payload-enhanced-sidebar-active-tab'\n\nimport type {\n EnhancedSidebarConfig,\n ExtendedEntity,\n ExtendedGroup,\n SidebarTab,\n SidebarTabContent as SidebarTabContentType,\n SidebarTabItem,\n} from '../../types'\n\nimport { extractLocalizedValue, resolveSidebarComponent } from '../../utils'\nimport { getNavPrefs } from './getNavPrefs'\nimport { Icon } from './Icon'\nimport { NavItem } from './NavItem'\nimport { SidebarContent } from './SidebarContent'\nimport './index.scss'\n\nexport type EnhancedSidebarProps = {\n req?: PayloadRequest\n sidebarConfig?: EnhancedSidebarConfig\n} & ServerProps\n\n/**\n * Computes filtered and merged groups for a specific tab (server-side).\n */\nconst computeGroupsForTab = (\n tab: SidebarTabContentType,\n groups: ExtendedGroup[],\n currentLang: string,\n): ExtendedGroup[] => {\n const { collections: tabCollections, customItems, globals: tabGlobals } = tab\n\n const showAll = !tabCollections && !tabGlobals\n const allowedSlugs = new Set([...(tabCollections ?? []), ...(tabGlobals ?? [])])\n\n let result: ExtendedGroup[] = []\n\n if (showAll) {\n result = groups.map((g) => ({ ...g, entities: [...g.entities] }))\n } else if (allowedSlugs.size > 0) {\n result = groups\n .map((group) => ({\n ...group,\n entities: group.entities.filter((entity) => allowedSlugs.has(entity.slug)),\n }))\n .filter((group) => group.entities.length > 0)\n }\n\n if (customItems && customItems.length > 0) {\n const topAdditions: ExtendedGroup[] = []\n const topUngrouped: SidebarTabItem[] = []\n const bottomUngrouped: SidebarTabItem[] = []\n\n const toEntity = (item: SidebarTabItem): ExtendedEntity =>\n ({\n slug: item.slug,\n type: 'custom',\n href: item.href,\n isExternal: item.isExternal,\n label: item.label,\n }) as ExtendedEntity\n\n for (const item of customItems) {\n if (item.group) {\n const itemGroupLabel = extractLocalizedValue(item.group, currentLang)\n const existingGroup = result.find((g) => {\n const groupLabel = extractLocalizedValue(g.label, currentLang)\n return groupLabel === itemGroupLabel\n })\n\n if (existingGroup) {\n // Merged into existing collection group — position has no effect here\n existingGroup.entities.push(toEntity(item))\n } else {\n // New custom group — position controls top vs bottom\n const newGroup: ExtendedGroup = { entities: [toEntity(item)], label: item.group }\n if (item.position === 'top') {\n topAdditions.push(newGroup)\n } else {\n result.push(newGroup)\n }\n }\n } else {\n if (item.position === 'top') {\n topUngrouped.push(item)\n } else {\n bottomUngrouped.push(item)\n }\n }\n }\n\n if (topUngrouped.length > 0) {\n topAdditions.unshift({ entities: topUngrouped.map(toEntity), label: '' })\n }\n\n if (bottomUngrouped.length > 0) {\n result.push({ entities: bottomUngrouped.map(toEntity), label: '' })\n }\n\n result = [...topAdditions, ...result]\n }\n\n return result\n}\n\nexport const EnhancedSidebar: React.FC<EnhancedSidebarProps> = async (props) => {\n const {\n i18n,\n locale,\n params,\n payload,\n permissions,\n req,\n searchParams,\n sidebarConfig,\n user,\n visibleEntities,\n documentSubViewType,\n viewType,\n } = props\n\n if (!payload?.config) {\n return null\n }\n\n const {\n admin: {\n components: { afterNavLinks, beforeNavLinks, settingsMenu },\n },\n collections,\n globals,\n } = payload.config\n\n const groups = groupNavItems(\n [\n ...collections\n .filter(({ slug }) => visibleEntities?.collections.includes(slug))\n .map(\n (collection) =>\n ({\n type: EntityType.collection,\n entity: collection,\n }) satisfies EntityToGroup,\n ),\n ...globals\n .filter(({ slug }) => visibleEntities?.globals.includes(slug))\n .map(\n (global) =>\n ({\n type: EntityType.global,\n entity: global,\n }) satisfies EntityToGroup,\n ),\n ],\n permissions || {},\n i18n,\n ) as unknown as ExtendedGroup[]\n\n const navPreferences = await getNavPrefs(req)\n\n const serverProps = {\n i18n,\n locale,\n params,\n payload,\n permissions,\n searchParams,\n user,\n }\n\n const clientProps = {\n documentSubViewType,\n viewType,\n }\n\n const beforeNavLinksRendered = RenderServerComponent({\n Component: beforeNavLinks,\n importMap: payload.importMap,\n serverProps,\n clientProps,\n })\n\n const afterNavLinksRendered = RenderServerComponent({\n Component: afterNavLinks,\n importMap: payload.importMap,\n serverProps,\n clientProps,\n })\n\n const renderedSettingsMenu =\n settingsMenu && Array.isArray(settingsMenu)\n ? settingsMenu.map((item, index) =>\n RenderServerComponent({\n Component: item,\n importMap: payload.importMap,\n key: `settings-menu-item-${index}`,\n serverProps,\n clientProps,\n }),\n )\n : []\n\n const config: EnhancedSidebarConfig = sidebarConfig ?? {\n tabs: [\n {\n id: 'default',\n type: 'tab',\n icon: 'LayoutGrid',\n label: 'Collections',\n },\n ],\n }\n\n // Read active tab from cookie\n const cookieStore = await cookies()\n const storedTabId = cookieStore.get(COOKIE_KEY)?.value\n const tabs = config.tabs?.filter((t) => t.type === 'tab') ?? []\n const defaultTabId = tabs[0]?.id ?? 'default'\n const initialActiveTabId =\n storedTabId && tabs.some((t) => t.id === storedTabId) ? storedTabId : defaultTabId\n\n const adminRoute = payload.config.routes.admin\n const currentLang = i18n.language\n\n /**\n * Renders a single entity as a NavItem (default or custom).\n */\n const renderEntity = (entity: ExtendedEntity, key: string): React.ReactNode => {\n const { slug, type } = entity\n let href: string\n let id: string\n\n if (type === 'collection') {\n href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })\n id = `nav-${slug}`\n } else if (type === 'global') {\n href = formatAdminURL({ adminRoute, path: `/globals/${slug}` })\n id = `nav-global-${slug}`\n } else if (type === 'custom' && entity.href) {\n id = `nav-custom-${slug}`\n href = entity.isExternal ? entity.href : formatAdminURL({ adminRoute, path: entity.href })\n } else {\n return null\n }\n\n const badgeConfig = config.badges?.[slug]\n\n if (config.customComponents?.NavItem) {\n const label = getTranslation(entity.label, i18n)\n const { path, clientProps: extraProps } = resolveSidebarComponent(config.customComponents.NavItem)\n return RenderServerComponent({\n Component: path,\n importMap: payload.importMap,\n key,\n clientProps: { entity, href, id, badgeConfig, label, ...extraProps },\n })\n }\n\n return <NavItem key={key} badgeConfig={badgeConfig} entity={entity} href={href} id={id} />\n }\n\n /**\n * Renders a group with its entities (default NavGroup or custom).\n */\n const renderGroup = (group: ExtendedGroup, key: string): React.ReactNode => {\n const { entities, label } = group\n const isUngrouped = !label || (typeof label === 'string' && label === '')\n const translatedLabel = getTranslation(label || '', i18n)\n\n const items = entities.map((entity, i) => renderEntity(entity, `${key}-${i}`))\n\n if (isUngrouped) {\n return <Fragment key={key}>{items}</Fragment>\n }\n\n if (config.customComponents?.NavGroup) {\n const { path, clientProps: extraProps } = resolveSidebarComponent(config.customComponents.NavGroup)\n return RenderServerComponent({\n Component: path,\n importMap: payload.importMap,\n key,\n clientProps: {\n label: translatedLabel,\n isOpen: navPreferences?.groups?.[translatedLabel]?.open,\n children: items,\n ...extraProps,\n },\n })\n }\n\n return (\n <NavGroup\n isOpen={navPreferences?.groups?.[translatedLabel]?.open}\n key={key}\n label={translatedLabel}\n >\n {items}\n </NavGroup>\n )\n }\n\n // Pre-render content for every tab on the server\n const tabsContent: Record<string, React.ReactNode> = {}\n for (const tab of tabs) {\n const tabGroups = computeGroupsForTab(tab, groups, currentLang)\n tabsContent[tab.id] = (\n <Fragment>{tabGroups.map((group, i) => renderGroup(group, `${tab.id}-${i}`))}</Fragment>\n )\n }\n\n // For the no-tabs fallback\n const allContent =\n tabs.length === 0 ? (\n <Fragment>{groups.map((group, i) => renderGroup(group, `all-${i}`))}</Fragment>\n ) : undefined\n\n // Build server-side icon and tab button rendering\n const allTabItems = config.tabs ?? []\n const hasCustomTabButton = !!config.customComponents?.TabButton\n const hasAnyIconComponent = allTabItems.some((t) => t.iconComponent)\n\n // tabIcons: per-id icon node (only built when no custom TabButton, just iconComponent overrides)\n const tabIcons: Record<string, React.ReactNode> = {}\n // renderedTabItems: fully custom tab button nodes (built when customComponents.TabButton is set)\n const renderedTabItems: React.ReactNode[] = []\n\n if (hasCustomTabButton || hasAnyIconComponent) {\n for (const item of allTabItems) {\n const label = getTranslation(item.label, i18n)\n\n // Resolve icon: custom iconComponent > default Lucide\n let iconNode: React.ReactNode\n if (item.iconComponent) {\n const { path: iconPath, clientProps: iconExtraProps } = resolveSidebarComponent(item.iconComponent)\n iconNode = RenderServerComponent({\n Component: iconPath,\n importMap: payload.importMap,\n clientProps: { id: item.id, label, type: item.type, ...iconExtraProps },\n })\n } else {\n iconNode = <Icon name={item.icon!} size={20} />\n }\n\n if (hasCustomTabButton) {\n // Compute href for links\n let href: string | undefined\n if (item.type === 'link') {\n href = item.isExternal\n ? item.href\n : formatAdminURL({ adminRoute, path: item.href })\n }\n\n const { path: tabBtnPath, clientProps: tabBtnExtraProps } = resolveSidebarComponent(config.customComponents!.TabButton!)\n renderedTabItems.push(\n RenderServerComponent({\n Component: tabBtnPath,\n importMap: payload.importMap,\n key: item.id,\n clientProps: {\n badge: item.badge,\n href,\n icon: iconNode,\n id: item.id,\n isExternal: item.type === 'link' ? item.isExternal : undefined,\n label,\n type: item.type,\n ...tabBtnExtraProps,\n },\n }),\n )\n } else if (item.iconComponent) {\n tabIcons[item.id] = iconNode\n }\n }\n }\n\n const customNavContent = config.customComponents?.NavContent\n ? (() => {\n const { path, clientProps: extraProps } = resolveSidebarComponent(config.customComponents!.NavContent!)\n return RenderServerComponent({\n Component: path,\n importMap: payload.importMap,\n key: 'enhanced-sidebar-nav-content',\n clientProps: {\n afterNavLinks: afterNavLinksRendered,\n allContent,\n beforeNavLinks: beforeNavLinksRendered,\n tabs: tabs.map((t) => ({ id: t.id })),\n tabsContent,\n ...extraProps,\n },\n })\n })()\n : undefined\n\n return (\n <SidebarContent\n afterNavLinks={afterNavLinksRendered}\n allContent={allContent}\n beforeNavLinks={beforeNavLinksRendered}\n customNavContent={customNavContent}\n initialActiveTabId={initialActiveTabId}\n renderedTabItems={renderedTabItems.length > 0 ? renderedTabItems : undefined}\n settingsMenu={renderedSettingsMenu}\n sidebarConfig={config}\n tabIcons={Object.keys(tabIcons).length > 0 ? tabIcons : undefined}\n tabsContent={tabsContent}\n />\n )\n}\n\nexport default EnhancedSidebar\n"],"names":["getTranslation","RenderServerComponent","NavGroup","EntityType","groupNavItems","cookies","formatAdminURL","React","Fragment","COOKIE_KEY","extractLocalizedValue","resolveSidebarComponent","getNavPrefs","Icon","NavItem","SidebarContent","computeGroupsForTab","tab","groups","currentLang","collections","tabCollections","customItems","globals","tabGlobals","showAll","allowedSlugs","Set","result","map","g","entities","size","group","filter","entity","has","slug","length","topAdditions","topUngrouped","bottomUngrouped","toEntity","item","type","href","isExternal","label","itemGroupLabel","existingGroup","find","groupLabel","push","newGroup","position","unshift","EnhancedSidebar","props","i18n","locale","params","payload","permissions","req","searchParams","sidebarConfig","user","visibleEntities","documentSubViewType","viewType","config","admin","components","afterNavLinks","beforeNavLinks","settingsMenu","includes","collection","global","navPreferences","serverProps","clientProps","beforeNavLinksRendered","Component","importMap","afterNavLinksRendered","renderedSettingsMenu","Array","isArray","index","key","tabs","id","icon","cookieStore","storedTabId","get","value","t","defaultTabId","initialActiveTabId","some","adminRoute","routes","language","renderEntity","path","badgeConfig","badges","customComponents","extraProps","renderGroup","isUngrouped","translatedLabel","items","i","isOpen","open","children","tabsContent","tabGroups","allContent","undefined","allTabItems","hasCustomTabButton","TabButton","hasAnyIconComponent","iconComponent","tabIcons","renderedTabItems","iconNode","iconPath","iconExtraProps","name","tabBtnPath","tabBtnExtraProps","badge","customNavContent","NavContent","Object","keys"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/EnhancedSidebar/index.tsx"],"sourcesContent":["import type { EntityToGroup } from '@payloadcms/ui/shared'\nimport type { PayloadRequest, ServerProps } from 'payload'\n\nimport { getTranslation } from '@payloadcms/translations'\nimport { NavGroup } from '@payloadcms/ui'\nimport { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'\nimport { EntityType, groupNavItems } from '@payloadcms/ui/shared'\nimport { cookies } from 'next/headers'\nimport { formatAdminURL } from 'payload/shared'\nimport React, { Fragment } from 'react'\n\nconst COOKIE_KEY = 'payload-enhanced-sidebar-active-tab'\n\nimport type {\n EnhancedSidebarConfig,\n ExtendedEntity,\n ExtendedGroup,\n SidebarTab,\n SidebarTabContent as SidebarTabContentType,\n SidebarTabItem,\n} from '../../types'\n\nimport { extractLocalizedValue, resolveSidebarComponent, sanitizeSidebarConfig } from '../../utils'\nimport { getNavPrefs } from './getNavPrefs'\nimport { Icon } from './Icon'\nimport { NavItem } from './NavItem'\nimport { SidebarContent } from './SidebarContent'\nimport './index.scss'\n\nexport type EnhancedSidebarProps = {\n req?: PayloadRequest\n sidebarConfig?: EnhancedSidebarConfig\n} & ServerProps\n\n/**\n * Computes filtered and merged groups for a specific tab (server-side).\n */\nconst computeGroupsForTab = async (\n tab: SidebarTabContentType,\n groups: ExtendedGroup[],\n currentLang: string,\n req: PayloadRequest | undefined,\n): Promise<ExtendedGroup[]> => {\n const { collections: tabCollections, customItems, globals: tabGlobals } = tab\n\n const showAll = !tabCollections && !tabGlobals\n const allowedSlugs = new Set([...(tabCollections ?? []), ...(tabGlobals ?? [])])\n\n let result: ExtendedGroup[] = []\n\n if (showAll) {\n result = groups.map((g) => ({ ...g, entities: [...g.entities] }))\n } else if (allowedSlugs.size > 0) {\n result = groups\n .map((group) => ({\n ...group,\n entities: group.entities.filter((entity) => allowedSlugs.has(entity.slug)),\n }))\n .filter((group) => group.entities.length > 0)\n }\n\n if (customItems && customItems.length > 0) {\n const topAdditions: ExtendedGroup[] = []\n const topUngrouped: SidebarTabItem[] = []\n const bottomUngrouped: SidebarTabItem[] = []\n\n const toEntity = (item: SidebarTabItem): ExtendedEntity =>\n ({\n slug: item.slug,\n type: 'custom',\n href: item.href,\n isExternal: item.isExternal,\n label: item.label,\n }) as ExtendedEntity\n\n // Filter custom items by access — fail-closed: missing req denies access\n const accessResults = await Promise.all(\n customItems.map((item) => (item.access ? (req ? item.access({ item, req }) : false) : true)),\n )\n const visibleItems = customItems.filter((_, i) => accessResults[i])\n\n for (const item of visibleItems) {\n if (item.group) {\n const itemGroupLabel = extractLocalizedValue(item.group, currentLang)\n const existingGroup = result.find((g) => {\n const groupLabel = extractLocalizedValue(g.label, currentLang)\n return groupLabel === itemGroupLabel\n })\n\n if (existingGroup) {\n // Merged into existing collection group — position has no effect here\n existingGroup.entities.push(toEntity(item))\n } else {\n // New custom group — position controls top vs bottom\n const newGroup: ExtendedGroup = { entities: [toEntity(item)], label: item.group }\n if (item.position === 'top') {\n topAdditions.push(newGroup)\n } else {\n result.push(newGroup)\n }\n }\n } else {\n if (item.position === 'top') {\n topUngrouped.push(item)\n } else {\n bottomUngrouped.push(item)\n }\n }\n }\n\n if (topUngrouped.length > 0) {\n topAdditions.unshift({ entities: topUngrouped.map(toEntity), label: '' })\n }\n\n if (bottomUngrouped.length > 0) {\n result.push({ entities: bottomUngrouped.map(toEntity), label: '' })\n }\n\n result = [...topAdditions, ...result]\n }\n\n return result\n}\n\nexport const EnhancedSidebar: React.FC<EnhancedSidebarProps> = async (props) => {\n const {\n documentSubViewType,\n i18n,\n locale,\n params,\n payload,\n permissions,\n req,\n searchParams,\n sidebarConfig,\n user,\n viewType,\n visibleEntities,\n } = props\n\n if (!payload?.config) {\n return null\n }\n\n const {\n admin: {\n components: { afterNavLinks, beforeNavLinks, settingsMenu },\n },\n collections,\n globals,\n } = payload.config\n\n const groups = groupNavItems(\n [\n ...collections\n .filter(({ slug }) => visibleEntities?.collections.includes(slug))\n .map(\n (collection) =>\n ({\n type: EntityType.collection,\n entity: collection,\n }) satisfies EntityToGroup,\n ),\n ...globals\n .filter(({ slug }) => visibleEntities?.globals.includes(slug))\n .map(\n (global) =>\n ({\n type: EntityType.global,\n entity: global,\n }) satisfies EntityToGroup,\n ),\n ],\n permissions || {},\n i18n,\n ) as unknown as ExtendedGroup[]\n\n const navPreferences = await getNavPrefs(req)\n\n const serverProps = {\n i18n,\n locale,\n params,\n payload,\n permissions,\n searchParams,\n user,\n }\n\n const clientProps = {\n documentSubViewType,\n viewType,\n }\n\n const beforeNavLinksRendered = RenderServerComponent({\n clientProps,\n Component: beforeNavLinks,\n importMap: payload.importMap,\n serverProps,\n })\n\n const afterNavLinksRendered = RenderServerComponent({\n clientProps,\n Component: afterNavLinks,\n importMap: payload.importMap,\n serverProps,\n })\n\n const renderedSettingsMenu =\n settingsMenu && Array.isArray(settingsMenu)\n ? settingsMenu.map((item, index) =>\n RenderServerComponent({\n clientProps,\n Component: item,\n importMap: payload.importMap,\n key: `settings-menu-item-${index}`,\n serverProps,\n }),\n )\n : []\n\n const config: EnhancedSidebarConfig = sidebarConfig ?? {\n tabs: [\n {\n id: 'default',\n type: 'tab',\n icon: 'LayoutGrid',\n label: 'Collections',\n },\n ],\n }\n\n // Filter all tab bar items by access — fail-closed: missing req denies access\n // Note: req can be undefined on 404 pages due to a Payload bug (NotFound view omits req from props)\n const allConfigTabs = config.tabs ?? []\n const tabAccessResults = await Promise.all(\n allConfigTabs.map((t) => (t.access ? (req ? t.access({ item: t, req }) : false) : true)),\n )\n const visibleTabItems = allConfigTabs.filter((_, i) => tabAccessResults[i])\n\n // Read active tab from cookie\n const cookieStore = await cookies()\n const storedTabId = cookieStore.get(COOKIE_KEY)?.value\n const tabs = visibleTabItems.filter((t) => t.type === 'tab') as SidebarTabContentType[]\n const defaultTabId = tabs[0]?.id ?? 'default'\n const initialActiveTabId =\n storedTabId && tabs.some((t) => t.id === storedTabId) ? storedTabId : defaultTabId\n\n const adminRoute = payload.config.routes.admin\n const currentLang = i18n.language\n\n /**\n * Renders a single entity as a NavItem (default or custom).\n */\n const renderEntity = (entity: ExtendedEntity, key: string): React.ReactNode => {\n const { slug, type } = entity\n let href: string\n let id: string\n\n if (type === 'collection') {\n href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })\n id = `nav-${slug}`\n } else if (type === 'global') {\n href = formatAdminURL({ adminRoute, path: `/globals/${slug}` })\n id = `nav-global-${slug}`\n } else if (type === 'custom' && entity.href) {\n id = `nav-custom-${slug}`\n href = entity.isExternal ? entity.href : formatAdminURL({ adminRoute, path: entity.href })\n } else {\n return null\n }\n\n const badgeConfig = config.badges?.[slug]\n\n if (config.customComponents?.NavItem) {\n const label = getTranslation(entity.label, i18n)\n const { clientProps: extraProps, path } = resolveSidebarComponent(\n config.customComponents.NavItem,\n )\n return RenderServerComponent({\n clientProps: { id, badgeConfig, entity, href, label, ...extraProps },\n Component: path,\n importMap: payload.importMap,\n key,\n serverProps,\n })\n }\n\n return <NavItem badgeConfig={badgeConfig} entity={entity} href={href} id={id} key={key} />\n }\n\n /**\n * Renders a group with its entities (default NavGroup or custom).\n */\n const renderGroup = (group: ExtendedGroup, key: string): React.ReactNode => {\n const { entities, label } = group\n const isUngrouped = !label || (typeof label === 'string' && label === '')\n const translatedLabel = getTranslation(label || '', i18n)\n\n const items = entities.map((entity, i) => renderEntity(entity, `${key}-${i}`))\n\n if (isUngrouped) {\n return <Fragment key={key}>{items}</Fragment>\n }\n\n if (config.customComponents?.NavGroup) {\n const { clientProps: extraProps, path } = resolveSidebarComponent(\n config.customComponents.NavGroup,\n )\n return RenderServerComponent({\n clientProps: {\n children: items,\n isOpen: navPreferences?.groups?.[translatedLabel]?.open,\n label: translatedLabel,\n ...extraProps,\n },\n Component: path,\n importMap: payload.importMap,\n key,\n serverProps,\n })\n }\n\n return (\n <NavGroup\n isOpen={navPreferences?.groups?.[translatedLabel]?.open}\n key={key}\n label={translatedLabel}\n >\n {items}\n </NavGroup>\n )\n }\n\n // Pre-render content for every tab on the server (computed in parallel)\n const tabGroupResults = await Promise.all(\n tabs.map((tab) => computeGroupsForTab(tab, groups, currentLang, req)),\n )\n const tabsContent: Record<string, React.ReactNode> = {}\n for (let i = 0; i < tabs.length; i++) {\n const tab = tabs[i]\n const tabGroups = tabGroupResults[i]\n tabsContent[tab.id] = (\n <Fragment>{tabGroups.map((group, j) => renderGroup(group, `${tab.id}-${j}`))}</Fragment>\n )\n }\n\n // For the no-tabs fallback\n const allContent =\n tabs.length === 0 ? (\n <Fragment>{groups.map((group, i) => renderGroup(group, `all-${i}`))}</Fragment>\n ) : undefined\n\n // Build server-side icon and tab button rendering\n const allTabItems = visibleTabItems\n const hasCustomTabButton = !!config.customComponents?.TabButton\n const hasAnyIconComponent = allTabItems.some((t) => t.iconComponent)\n\n // tabIcons: per-id icon node (only built when no custom TabButton, just iconComponent overrides)\n const tabIcons: Record<string, React.ReactNode> = {}\n // renderedTabItems: fully custom tab button nodes (built when customComponents.TabButton is set)\n const renderedTabItems: React.ReactNode[] = []\n\n if (hasCustomTabButton || hasAnyIconComponent) {\n for (const item of allTabItems) {\n const label = getTranslation(item.label, i18n)\n\n // Resolve icon: custom iconComponent > default Lucide\n let iconNode: React.ReactNode\n if (item.iconComponent) {\n const { clientProps: iconExtraProps, path: iconPath } = resolveSidebarComponent(\n item.iconComponent,\n )\n iconNode = RenderServerComponent({\n clientProps: { id: item.id, type: item.type, label, ...iconExtraProps },\n Component: iconPath,\n importMap: payload.importMap,\n serverProps,\n })\n } else {\n iconNode = item.icon ? <Icon name={item.icon} size={20} /> : null\n }\n\n if (hasCustomTabButton) {\n // Compute href for links\n let href: string | undefined\n if (item.type === 'link') {\n href = item.isExternal ? item.href : formatAdminURL({ adminRoute, path: item.href })\n }\n\n const { clientProps: tabBtnExtraProps, path: tabBtnPath } = resolveSidebarComponent(\n config.customComponents!.TabButton!,\n )\n renderedTabItems.push(\n RenderServerComponent({\n clientProps: {\n id: item.id,\n type: item.type,\n badge: item.badge,\n href,\n icon: iconNode,\n isExternal: item.type === 'link' ? item.isExternal : undefined,\n label,\n ...tabBtnExtraProps,\n },\n Component: tabBtnPath,\n importMap: payload.importMap,\n key: item.id,\n serverProps,\n }),\n )\n } else if (item.iconComponent) {\n tabIcons[item.id] = iconNode\n }\n }\n }\n\n const customNavContent = config.customComponents?.NavContent\n ? (() => {\n const { clientProps: extraProps, path } = resolveSidebarComponent(\n config.customComponents.NavContent,\n )\n return RenderServerComponent({\n clientProps: {\n afterNavLinks: afterNavLinksRendered,\n allContent,\n beforeNavLinks: beforeNavLinksRendered,\n tabs: tabs.map((t) => ({ id: t.id })),\n tabsContent,\n ...extraProps,\n },\n Component: path,\n importMap: payload.importMap,\n key: 'enhanced-sidebar-nav-content',\n serverProps,\n })\n })()\n : undefined\n\n // Strip access functions and use only visible tabs before passing to client component\n const clientSidebarConfig = sanitizeSidebarConfig({ ...config, tabs: visibleTabItems })\n\n return (\n <SidebarContent\n afterNavLinks={afterNavLinksRendered}\n allContent={allContent}\n beforeNavLinks={beforeNavLinksRendered}\n customNavContent={customNavContent}\n initialActiveTabId={initialActiveTabId}\n renderedTabItems={renderedTabItems.length > 0 ? renderedTabItems : undefined}\n settingsMenu={renderedSettingsMenu}\n sidebarConfig={clientSidebarConfig}\n tabIcons={Object.keys(tabIcons).length > 0 ? tabIcons : undefined}\n tabsContent={tabsContent}\n />\n )\n}\n\nexport default EnhancedSidebar\n"],"names":["getTranslation","NavGroup","RenderServerComponent","EntityType","groupNavItems","cookies","formatAdminURL","React","Fragment","COOKIE_KEY","extractLocalizedValue","resolveSidebarComponent","sanitizeSidebarConfig","getNavPrefs","Icon","NavItem","SidebarContent","computeGroupsForTab","tab","groups","currentLang","req","collections","tabCollections","customItems","globals","tabGlobals","showAll","allowedSlugs","Set","result","map","g","entities","size","group","filter","entity","has","slug","length","topAdditions","topUngrouped","bottomUngrouped","toEntity","item","type","href","isExternal","label","accessResults","Promise","all","access","visibleItems","_","i","itemGroupLabel","existingGroup","find","groupLabel","push","newGroup","position","unshift","EnhancedSidebar","props","documentSubViewType","i18n","locale","params","payload","permissions","searchParams","sidebarConfig","user","viewType","visibleEntities","config","admin","components","afterNavLinks","beforeNavLinks","settingsMenu","includes","collection","global","navPreferences","serverProps","clientProps","beforeNavLinksRendered","Component","importMap","afterNavLinksRendered","renderedSettingsMenu","Array","isArray","index","key","tabs","id","icon","allConfigTabs","tabAccessResults","t","visibleTabItems","cookieStore","storedTabId","get","value","defaultTabId","initialActiveTabId","some","adminRoute","routes","language","renderEntity","path","badgeConfig","badges","customComponents","extraProps","renderGroup","isUngrouped","translatedLabel","items","children","isOpen","open","tabGroupResults","tabsContent","tabGroups","j","allContent","undefined","allTabItems","hasCustomTabButton","TabButton","hasAnyIconComponent","iconComponent","tabIcons","renderedTabItems","iconNode","iconExtraProps","iconPath","name","tabBtnExtraProps","tabBtnPath","badge","customNavContent","NavContent","clientSidebarConfig","Object","keys"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -5,4 +5,4 @@ export { BadgeProvider, useBadgeContext, useBadgeValue, } from './components/Enh
5
5
  export { useEnhancedSidebar } from './components/EnhancedSidebar/context';
6
6
  export { useNavItemState } from './components/EnhancedSidebar/hooks/useNavItemState';
7
7
  export { useTabState } from './components/EnhancedSidebar/hooks/useTabState';
8
- export type { BadgeColor, BadgeConfig, BadgeConfigApi, BadgeConfigCollectionCount, BadgeConfigProvider, BadgeValues, CustomNavContentProps, CustomNavGroupProps, CustomNavItemProps, CustomTabButtonProps, CustomTabIconProps, EnhancedSidebarConfig, SidebarComponent, } from './types';
8
+ export type { BadgeColor, BadgeConfig, BadgeConfigApi, BadgeConfigCollectionCount, BadgeConfigProvider, BadgeValues, CustomNavContentProps, CustomNavGroupProps, CustomNavItemProps, CustomTabButtonProps, CustomTabIconProps, EnhancedSidebarConfig, ItemAccessFunction, SidebarComponent, TabAccessFunction, } from './types';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { deepMerge } from 'payload';
2
2
  import { sidebarTranslations } from './translations';
3
- import { resolveSidebarComponent } from './utils';
3
+ import { resolveSidebarComponent, sanitizeSidebarConfig } from './utils';
4
4
  /**
5
5
  * Default configuration for the enhanced sidebar
6
6
  */ const defaultConfig = {
@@ -42,14 +42,13 @@ export const payloadEnhancedSidebar = (pluginOptions = {})=>(config)=>{
42
42
  if (!config.admin.components) {
43
43
  config.admin.components = {};
44
44
  }
45
- if (!config.admin.components.Nav) {
46
- config.admin.components.Nav = {
47
- path: '@veiag/payload-enhanced-sidebar/rsc#EnhancedSidebar',
48
- serverProps: {
49
- sidebarConfig
50
- }
51
- };
52
- }
45
+ // Always override Nav — user-defined Nav in config should use customComponents instead
46
+ config.admin.components.Nav = {
47
+ path: '@veiag/payload-enhanced-sidebar/rsc#EnhancedSidebar',
48
+ serverProps: {
49
+ sidebarConfig
50
+ }
51
+ };
53
52
  // Register custom components and per-tab icons in the import map
54
53
  if (!config.admin.dependencies) {
55
54
  config.admin.dependencies = {};
@@ -61,7 +60,7 @@ export const payloadEnhancedSidebar = (pluginOptions = {})=>(config)=>{
61
60
  'TabButton'
62
61
  ];
63
62
  for (const slot of customComponentSlots){
64
- const component = pluginOptions.customComponents?.[slot];
63
+ const component = sidebarConfig.customComponents?.[slot];
65
64
  if (component) {
66
65
  const { path } = resolveSidebarComponent(component);
67
66
  config.admin.dependencies[`enhanced-sidebar-${slot.toLowerCase()}`] = {
@@ -70,7 +69,12 @@ export const payloadEnhancedSidebar = (pluginOptions = {})=>(config)=>{
70
69
  };
71
70
  }
72
71
  }
72
+ const seenTabIds = new Set();
73
73
  for (const tab of sidebarConfig.tabs ?? []){
74
+ if (seenTabIds.has(tab.id)) {
75
+ throw new Error(`[payload-enhanced-sidebar] Duplicate tab id "${tab.id}". Each tab must have a unique id.`);
76
+ }
77
+ seenTabIds.add(tab.id);
74
78
  if (tab.iconComponent) {
75
79
  const { path } = resolveSidebarComponent(tab.iconComponent);
76
80
  config.admin.dependencies[`enhanced-sidebar-icon-${tab.id}`] = {
@@ -89,7 +93,7 @@ export const payloadEnhancedSidebar = (pluginOptions = {})=>(config)=>{
89
93
  // Add our internal provider at the beginning (so user providers can override)
90
94
  config.admin.components.providers.unshift({
91
95
  clientProps: {
92
- sidebarConfig
96
+ sidebarConfig: sanitizeSidebarConfig(sidebarConfig)
93
97
  },
94
98
  path: '@veiag/payload-enhanced-sidebar/client#InternalBadgeProvider'
95
99
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { type Config, deepMerge } from 'payload'\n\nimport type { EnhancedSidebarConfig } from './types'\n\nimport { sidebarTranslations } from './translations'\nimport { resolveSidebarComponent } from './utils'\n\n/**\n * Default configuration for the enhanced sidebar\n */\nconst defaultConfig: EnhancedSidebarConfig = {\n showLogout: true,\n tabs: [\n {\n id: 'dashboard',\n type: 'link',\n href: '/',\n icon: 'House',\n label: { en: 'Dashboard', uk: 'Головна' },\n },\n {\n id: 'default',\n type: 'tab',\n icon: 'LayoutGrid',\n label: { en: 'Collections', uk: 'Колекції' },\n },\n ],\n}\n\nexport const payloadEnhancedSidebar =\n (pluginOptions: EnhancedSidebarConfig = {}) =>\n (config: Config): Config => {\n if (pluginOptions.disabled) {\n return config\n }\n\n const sidebarConfig: EnhancedSidebarConfig = {\n ...defaultConfig,\n ...pluginOptions,\n tabs: pluginOptions.tabs ?? defaultConfig.tabs,\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.Nav) {\n config.admin.components.Nav = {\n path: '@veiag/payload-enhanced-sidebar/rsc#EnhancedSidebar',\n serverProps: {\n sidebarConfig,\n },\n }\n }\n\n // Register custom components and per-tab icons in the import map\n if (!config.admin.dependencies) {\n config.admin.dependencies = {}\n }\n\n const customComponentSlots = ['NavContent', 'NavGroup', 'NavItem', 'TabButton'] as const\n for (const slot of customComponentSlots) {\n const component = pluginOptions.customComponents?.[slot]\n if (component) {\n const { path } = resolveSidebarComponent(component)\n config.admin.dependencies[`enhanced-sidebar-${slot.toLowerCase()}`] = {\n type: 'component',\n path,\n }\n }\n }\n\n for (const tab of sidebarConfig.tabs ?? []) {\n if (tab.iconComponent) {\n const { path } = resolveSidebarComponent(tab.iconComponent)\n config.admin.dependencies[`enhanced-sidebar-icon-${tab.id}`] = {\n type: 'component',\n path,\n }\n }\n }\n\n // Check if we have any badges to fetch (api or collection-count)\n const hasBadgesToFetch =\n sidebarConfig.badges ||\n sidebarConfig.tabs?.some((tab) => tab.badge && tab.badge.type !== 'provider')\n\n // Add InternalBadgeProvider if we have badges to fetch\n if (hasBadgesToFetch) {\n if (!config.admin.components.providers) {\n config.admin.components.providers = []\n }\n\n // Add our internal provider at the beginning (so user providers can override)\n config.admin.components.providers.unshift({\n clientProps: {\n sidebarConfig,\n },\n path: '@veiag/payload-enhanced-sidebar/client#InternalBadgeProvider',\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, sidebarTranslations)\n\n return config\n }\n\nexport {\n BadgeProvider,\n useBadgeContext,\n useBadgeValue,\n} from './components/EnhancedSidebar/BadgeProvider'\n\nexport { useEnhancedSidebar } from './components/EnhancedSidebar/context'\nexport { useNavItemState } from './components/EnhancedSidebar/hooks/useNavItemState'\nexport { useTabState } from './components/EnhancedSidebar/hooks/useTabState'\n\nexport type {\n BadgeColor,\n BadgeConfig,\n BadgeConfigApi,\n BadgeConfigCollectionCount,\n BadgeConfigProvider,\n BadgeValues,\n CustomNavContentProps,\n CustomNavGroupProps,\n CustomNavItemProps,\n CustomTabButtonProps,\n CustomTabIconProps,\n EnhancedSidebarConfig,\n SidebarComponent,\n} from './types'\n"],"names":["deepMerge","sidebarTranslations","resolveSidebarComponent","defaultConfig","showLogout","tabs","id","type","href","icon","label","en","uk","payloadEnhancedSidebar","pluginOptions","config","disabled","sidebarConfig","admin","components","Nav","path","serverProps","dependencies","customComponentSlots","slot","component","customComponents","toLowerCase","tab","iconComponent","hasBadgesToFetch","badges","some","badge","providers","unshift","clientProps","i18n","translations","BadgeProvider","useBadgeContext","useBadgeValue","useEnhancedSidebar","useNavItemState","useTabState"],"mappings":"AAAA,SAAsBA,SAAS,QAAQ,UAAS;AAIhD,SAASC,mBAAmB,QAAQ,iBAAgB;AACpD,SAASC,uBAAuB,QAAQ,UAAS;AAEjD;;CAEC,GACD,MAAMC,gBAAuC;IAC3CC,YAAY;IACZC,MAAM;QACJ;YACEC,IAAI;YACJC,MAAM;YACNC,MAAM;YACNC,MAAM;YACNC,OAAO;gBAAEC,IAAI;gBAAaC,IAAI;YAAU;QAC1C;QACA;YACEN,IAAI;YACJC,MAAM;YACNE,MAAM;YACNC,OAAO;gBAAEC,IAAI;gBAAeC,IAAI;YAAW;QAC7C;KACD;AACH;AAEA,OAAO,MAAMC,yBACX,CAACC,gBAAuC,CAAC,CAAC,GAC1C,CAACC;QACC,IAAID,cAAcE,QAAQ,EAAE;YAC1B,OAAOD;QACT;QAEA,MAAME,gBAAuC;YAC3C,GAAGd,aAAa;YAChB,GAAGW,aAAa;YAChBT,MAAMS,cAAcT,IAAI,IAAIF,cAAcE,IAAI;QAChD;QAEA,IAAI,CAACU,OAAOG,KAAK,EAAE;YACjBH,OAAOG,KAAK,GAAG,CAAC;QAClB;QAEA,IAAI,CAACH,OAAOG,KAAK,CAACC,UAAU,EAAE;YAC5BJ,OAAOG,KAAK,CAACC,UAAU,GAAG,CAAC;QAC7B;QAEA,IAAI,CAACJ,OAAOG,KAAK,CAACC,UAAU,CAACC,GAAG,EAAE;YAChCL,OAAOG,KAAK,CAACC,UAAU,CAACC,GAAG,GAAG;gBAC5BC,MAAM;gBACNC,aAAa;oBACXL;gBACF;YACF;QACF;QAEA,iEAAiE;QACjE,IAAI,CAACF,OAAOG,KAAK,CAACK,YAAY,EAAE;YAC9BR,OAAOG,KAAK,CAACK,YAAY,GAAG,CAAC;QAC/B;QAEA,MAAMC,uBAAuB;YAAC;YAAc;YAAY;YAAW;SAAY;QAC/E,KAAK,MAAMC,QAAQD,qBAAsB;YACvC,MAAME,YAAYZ,cAAca,gBAAgB,EAAE,CAACF,KAAK;YACxD,IAAIC,WAAW;gBACb,MAAM,EAAEL,IAAI,EAAE,GAAGnB,wBAAwBwB;gBACzCX,OAAOG,KAAK,CAACK,YAAY,CAAC,CAAC,iBAAiB,EAAEE,KAAKG,WAAW,IAAI,CAAC,GAAG;oBACpErB,MAAM;oBACNc;gBACF;YACF;QACF;QAEA,KAAK,MAAMQ,OAAOZ,cAAcZ,IAAI,IAAI,EAAE,CAAE;YAC1C,IAAIwB,IAAIC,aAAa,EAAE;gBACrB,MAAM,EAAET,IAAI,EAAE,GAAGnB,wBAAwB2B,IAAIC,aAAa;gBAC1Df,OAAOG,KAAK,CAACK,YAAY,CAAC,CAAC,sBAAsB,EAAEM,IAAIvB,EAAE,EAAE,CAAC,GAAG;oBAC7DC,MAAM;oBACNc;gBACF;YACF;QACF;QAEA,iEAAiE;QACjE,MAAMU,mBACJd,cAAce,MAAM,IACpBf,cAAcZ,IAAI,EAAE4B,KAAK,CAACJ,MAAQA,IAAIK,KAAK,IAAIL,IAAIK,KAAK,CAAC3B,IAAI,KAAK;QAEpE,uDAAuD;QACvD,IAAIwB,kBAAkB;YACpB,IAAI,CAAChB,OAAOG,KAAK,CAACC,UAAU,CAACgB,SAAS,EAAE;gBACtCpB,OAAOG,KAAK,CAACC,UAAU,CAACgB,SAAS,GAAG,EAAE;YACxC;YAEA,8EAA8E;YAC9EpB,OAAOG,KAAK,CAACC,UAAU,CAACgB,SAAS,CAACC,OAAO,CAAC;gBACxCC,aAAa;oBACXpB;gBACF;gBACAI,MAAM;YACR;QACF;QAEA,sBAAsB;QACtB,IAAI,CAACN,OAAOuB,IAAI,EAAE;YAChBvB,OAAOuB,IAAI,GAAG,CAAC;QACjB;QACA,IAAI,CAACvB,OAAOuB,IAAI,CAACC,YAAY,EAAE;YAC7BxB,OAAOuB,IAAI,CAACC,YAAY,GAAG,CAAC;QAC9B;QAEAxB,OAAOuB,IAAI,CAACC,YAAY,GAAGvC,UAAUe,OAAOuB,IAAI,CAACC,YAAY,EAAEtC;QAE/D,OAAOc;IACT,EAAC;AAEH,SACEyB,aAAa,EACbC,eAAe,EACfC,aAAa,QACR,6CAA4C;AAEnD,SAASC,kBAAkB,QAAQ,uCAAsC;AACzE,SAASC,eAAe,QAAQ,qDAAoD;AACpF,SAASC,WAAW,QAAQ,iDAAgD"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { type Config, deepMerge } from 'payload'\n\nimport type { EnhancedSidebarConfig } from './types'\n\nimport { sidebarTranslations } from './translations'\nimport { resolveSidebarComponent, sanitizeSidebarConfig } from './utils'\n\n/**\n * Default configuration for the enhanced sidebar\n */\nconst defaultConfig: EnhancedSidebarConfig = {\n showLogout: true,\n tabs: [\n {\n id: 'dashboard',\n type: 'link',\n href: '/',\n icon: 'House',\n label: { en: 'Dashboard', uk: 'Головна' },\n },\n {\n id: 'default',\n type: 'tab',\n icon: 'LayoutGrid',\n label: { en: 'Collections', uk: 'Колекції' },\n },\n ],\n}\n\nexport const payloadEnhancedSidebar =\n (pluginOptions: EnhancedSidebarConfig = {}) =>\n (config: Config): Config => {\n if (pluginOptions.disabled) {\n return config\n }\n\n const sidebarConfig: EnhancedSidebarConfig = {\n ...defaultConfig,\n ...pluginOptions,\n tabs: pluginOptions.tabs ?? defaultConfig.tabs,\n }\n\n if (!config.admin) {\n config.admin = {}\n }\n\n if (!config.admin.components) {\n config.admin.components = {}\n }\n\n // Always override Nav — user-defined Nav in config should use customComponents instead\n config.admin.components.Nav = {\n path: '@veiag/payload-enhanced-sidebar/rsc#EnhancedSidebar',\n serverProps: {\n sidebarConfig,\n },\n }\n\n // Register custom components and per-tab icons in the import map\n if (!config.admin.dependencies) {\n config.admin.dependencies = {}\n }\n\n const customComponentSlots = ['NavContent', 'NavGroup', 'NavItem', 'TabButton'] as const\n for (const slot of customComponentSlots) {\n const component = sidebarConfig.customComponents?.[slot]\n if (component) {\n const { path } = resolveSidebarComponent(component)\n config.admin.dependencies[`enhanced-sidebar-${slot.toLowerCase()}`] = {\n type: 'component',\n path,\n }\n }\n }\n\n const seenTabIds = new Set<string>()\n for (const tab of sidebarConfig.tabs ?? []) {\n if (seenTabIds.has(tab.id)) {\n throw new Error(\n `[payload-enhanced-sidebar] Duplicate tab id \"${tab.id}\". Each tab must have a unique id.`,\n )\n }\n seenTabIds.add(tab.id)\n\n if (tab.iconComponent) {\n const { path } = resolveSidebarComponent(tab.iconComponent)\n config.admin.dependencies[`enhanced-sidebar-icon-${tab.id}`] = {\n type: 'component',\n path,\n }\n }\n }\n\n // Check if we have any badges to fetch (api or collection-count)\n const hasBadgesToFetch =\n sidebarConfig.badges ||\n sidebarConfig.tabs?.some((tab) => tab.badge && tab.badge.type !== 'provider')\n\n // Add InternalBadgeProvider if we have badges to fetch\n if (hasBadgesToFetch) {\n if (!config.admin.components.providers) {\n config.admin.components.providers = []\n }\n\n // Add our internal provider at the beginning (so user providers can override)\n config.admin.components.providers.unshift({\n clientProps: {\n sidebarConfig: sanitizeSidebarConfig(sidebarConfig),\n },\n path: '@veiag/payload-enhanced-sidebar/client#InternalBadgeProvider',\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, sidebarTranslations)\n\n return config\n }\n\nexport {\n BadgeProvider,\n useBadgeContext,\n useBadgeValue,\n} from './components/EnhancedSidebar/BadgeProvider'\n\nexport { useEnhancedSidebar } from './components/EnhancedSidebar/context'\nexport { useNavItemState } from './components/EnhancedSidebar/hooks/useNavItemState'\nexport { useTabState } from './components/EnhancedSidebar/hooks/useTabState'\n\nexport type {\n BadgeColor,\n BadgeConfig,\n BadgeConfigApi,\n BadgeConfigCollectionCount,\n BadgeConfigProvider,\n BadgeValues,\n CustomNavContentProps,\n CustomNavGroupProps,\n CustomNavItemProps,\n CustomTabButtonProps,\n CustomTabIconProps,\n EnhancedSidebarConfig,\n ItemAccessFunction,\n SidebarComponent,\n TabAccessFunction,\n} from './types'\n"],"names":["deepMerge","sidebarTranslations","resolveSidebarComponent","sanitizeSidebarConfig","defaultConfig","showLogout","tabs","id","type","href","icon","label","en","uk","payloadEnhancedSidebar","pluginOptions","config","disabled","sidebarConfig","admin","components","Nav","path","serverProps","dependencies","customComponentSlots","slot","component","customComponents","toLowerCase","seenTabIds","Set","tab","has","Error","add","iconComponent","hasBadgesToFetch","badges","some","badge","providers","unshift","clientProps","i18n","translations","BadgeProvider","useBadgeContext","useBadgeValue","useEnhancedSidebar","useNavItemState","useTabState"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { icons, LucideIcon } from 'lucide-react';
2
- import type { CollectionSlug, GlobalSlug, Where } from 'payload';
2
+ import type { CollectionSlug, GlobalSlug, PayloadRequest, Where } from 'payload';
3
3
  import type { ReactNode } from 'react';
4
4
  export type IconName = keyof typeof icons;
5
5
  export type LocalizedString = {
@@ -84,6 +84,22 @@ export type BadgeConfig = BadgeConfigApi | BadgeConfigCollectionCount | BadgeCon
84
84
  * Badge values provided via BadgeProvider context
85
85
  */
86
86
  export type BadgeValues = Record<string, number | ReactNode>;
87
+ /**
88
+ * Access function for a tab or link in the tabs bar.
89
+ * Return `false` to hide the item entirely (both the button and its content).
90
+ */
91
+ export type TabAccessFunction = (args: {
92
+ item: SidebarTab;
93
+ req: PayloadRequest;
94
+ }) => boolean | Promise<boolean>;
95
+ /**
96
+ * Access function for a custom item inside a tab.
97
+ * Return `false` to hide the item from the nav.
98
+ */
99
+ export type ItemAccessFunction = (args: {
100
+ item: SidebarTabItem;
101
+ req: PayloadRequest;
102
+ }) => boolean | Promise<boolean>;
87
103
  /**
88
104
  * Path to a component, either as a plain string or as an object with a path and
89
105
  * additional client props to forward to the component. Follows Payload's component format.
@@ -126,6 +142,11 @@ type TabIconConfig = {
126
142
  * Sidebar tab that shows content when selected
127
143
  */
128
144
  export type SidebarTabContent = {
145
+ /**
146
+ * Access control function. Called server-side with the current request.
147
+ * Return `false` to hide this tab (button + content) entirely.
148
+ */
149
+ access?: TabAccessFunction;
129
150
  /**
130
151
  * Badge configuration for this tab.
131
152
  * Shows a badge on the tab icon in the tabs bar.
@@ -156,6 +177,11 @@ export type SidebarTabContent = {
156
177
  type: 'tab';
157
178
  } & TabIconConfig;
158
179
  type SidebarTabLinkBase = {
180
+ /**
181
+ * Access control function. Called server-side with the current request.
182
+ * Return `false` to hide this link entirely.
183
+ */
184
+ access?: TabAccessFunction;
159
185
  /**
160
186
  * Badge configuration for this link.
161
187
  * Shows a badge on the link icon in the tabs bar.
@@ -186,6 +212,11 @@ export type SidebarTabLink = SidebarTabLinkExternal | SidebarTabLinkInternal;
186
212
  */
187
213
  export type SidebarTab = SidebarTabContent | SidebarTabLink;
188
214
  interface BaseSidebarTabItem {
215
+ /**
216
+ * Access control function. Called server-side with the current request.
217
+ * Return `false` to hide this item from the nav.
218
+ */
219
+ access?: ItemAccessFunction;
189
220
  /**
190
221
  * Group to add this item to.
191
222
  * If matches an existing collection group label, item will be merged into that group.
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, Where } from 'payload'\nimport type { ReactNode } from 'react'\n\nexport type IconName = keyof typeof icons\n\nexport type LocalizedString = { [locale: string]: string } | string\n\nexport type InternalIcon = IconName | LucideIcon\n\n// ============================================\n// Badge Types\n// ============================================\n\n/**\n * Available badge color variants\n */\nexport type BadgeColor = 'default' | 'error' | 'primary' | 'success' | 'warning'\n\n/**\n * Badge configuration Trr API-based fetching\n */\nexport interface BadgeConfigApi {\n /**\n * Badge color variant\n * @default 'default'\n */\n color?: BadgeColor\n /**\n * API endpoint to fetch badge data from.\n * Can be relative (to current origin) or absolute URL.\n */\n endpoint: string\n /**\n * HTTP method for the request\n * @default 'GET'\n */\n method?: 'GET' | 'POST'\n /**\n * Key in the response object to extract the count from\n * @default 'count'\n */\n responseKey?: string\n type: 'api'\n}\n\n/**\n * Badge configuration for provider-based values.\n * Values are provided via BadgeProvider context.\n */\nexport interface BadgeConfigProvider {\n /**\n * Badge color variant\n * @default 'default'\n */\n color?: BadgeColor\n /**\n * Slug to look up in the BadgeProvider values.\n * If not specified, defaults to the item's id/slug.\n */\n slug?: string\n type: 'provider'\n}\n\n/**\n * Badge configuration for automatic collection document count.\n * Fetches from /api/{collectionSlug}?limit=0 and uses totalDocs.\n */\nexport interface BadgeConfigCollectionCount {\n /**\n * Collection slug to count documents from.\n * If not specified, defaults to the item's slug.\n */\n collectionSlug?: CollectionSlug\n /**\n * Badge color variant\n * @default 'default'\n */\n color?: BadgeColor\n type: 'collection-count'\n /**\n * Optional where query to filter documents.\n * Will be serialized as query string.\n * @example { status: { equals: 'draft' } }\n */\n where?: Where\n}\n\n/**\n * Badge configuration - union of all badge types\n */\nexport type BadgeConfig = BadgeConfigApi | BadgeConfigCollectionCount | BadgeConfigProvider\n\n/**\n * Badge values provided via BadgeProvider context\n */\nexport type BadgeValues = Record<string, number | ReactNode>\n\n// ============================================\n// Enhanced Sidebar Types\n// ============================================\n\n/**\n * Path to a component, either as a plain string or as an object with a path and\n * additional client props to forward to the component. Follows Payload's component format.\n *\n * @example\n * ```typescript\n * // Simple path\n * iconComponent: './components/Icons#TabIcon'\n *\n * // With client props — useful for a single component shared across all tabs\n * iconComponent: {\n * path: './components/Icons#TabIcon',\n * clientProps: { icon: 'house' },\n * }\n * ```\n */\nexport type SidebarComponent =\n | {\n /** Additional props forwarded to the component on the client */\n clientProps?: Record<string, unknown>\n /** Component path in Payload's format: `'./path/to/file#ExportName'` */\n path: string\n }\n | string\n\n/**\n * Mutually exclusive icon config: either a Lucide icon name OR a custom icon component path.\n */\ntype TabIconConfig =\n | {\n /** Icon name from lucide-react */\n icon: IconName\n iconComponent?: never\n }\n | {\n icon?: never\n /**\n * Path to a custom icon component. Receives `CustomTabIconProps`.\n * Supports a plain string path or `{ path, clientProps }` to forward extra props.\n * Registered automatically in the import map.\n */\n iconComponent: SidebarComponent\n }\n\n/**\n * Sidebar tab that shows content when selected\n */\nexport type SidebarTabContent = {\n /**\n * Badge configuration for this tab.\n * Shows a badge on the tab icon in the tabs bar.\n */\n badge?: BadgeConfig\n /**\n * Collections to show in this tab.\n * If not specified, no collections are shown (unless items are specified).\n * Use collection slugs.\n */\n collections?: CollectionSlug[]\n /**\n * Custom items to add to this tab.\n * Items with `group` will be merged into matching collection groups.\n * Items without `group` will be shown at the bottom as a flat list.\n */\n customItems?: SidebarTabItem[]\n /**\n * Globals to show in this tab.\n * If not specified, no globals are shown.\n * Use global slugs.\n */\n globals?: GlobalSlug[]\n /** Unique identifier for the tab */\n id: string\n /** Tooltip/label for the tab */\n label: LocalizedString\n type: 'tab'\n} & TabIconConfig\n\ntype SidebarTabLinkBase = {\n /**\n * Badge configuration for this link.\n * Shows a badge on the link icon in the tabs bar.\n */\n badge?: BadgeConfig\n /** Unique identifier */\n id: string\n /** Tooltip/label */\n label: LocalizedString\n type: 'link'\n} & TabIconConfig\ntype SidebarTabLinkExternal = {\n /** Link href (absolute URL) */\n href: string\n isExternal: true\n} & SidebarTabLinkBase\ntype SidebarTabLinkInternal = {\n /** Link href (relative to admin route) */\n href: '' | `/${string}`\n isExternal?: false\n} & SidebarTabLinkBase\n/**\n * Sidebar link that navigates to a URL (not a tab)\n */\nexport type SidebarTabLink = SidebarTabLinkExternal | SidebarTabLinkInternal\n/**\n * A tab or link in the sidebar tabs bar\n */\nexport type SidebarTab = SidebarTabContent | SidebarTabLink\n\ninterface BaseSidebarTabItem {\n /**\n * Group to add this item to.\n * If matches an existing collection group label, item will be merged into that group.\n * If no match found, a new group will be created with this label.\n * If not specified, item will be shown as ungrouped (position controlled by `position`).\n */\n group?: LocalizedString\n /** Display label */\n label: LocalizedString\n /**\n * Where to place this item relative to collection groups in the tab.\n * - `'top'` — appears above all collection/global groups\n * - `'bottom'` — appears below all groups (default)\n *\n * Has no effect on items that are merged into an existing collection group via `group`.\n * For new custom groups (unmatched `group` label), controls whether the group appears\n * at the top or bottom of the nav.\n *\n * @default 'bottom'\n */\n position?: 'bottom' | 'top'\n /** Unique slug for the item */\n slug: string\n}\ninterface ExternalHrefItem extends BaseSidebarTabItem {\n /** Link href (absolute URL or relative to root) */\n href: string\n /** Whether the link is external, without admin route prefix. */\n isExternal: true\n}\n\ninterface InternalHrefItem extends BaseSidebarTabItem {\n /** Link href (relative to admin route) */\n href: '' | `/${string}`\n /** Whether the link is external, without admin route prefix. */\n isExternal?: false\n}\n/**\n * Custom item inside a sidebar tab\n */\nexport type SidebarTabItem = ExternalHrefItem | InternalHrefItem\n\n// ============================================\n// Custom Component Types\n// ============================================\n\n/**\n * Props received by a custom NavItem component registered via `customComponents.NavItem`.\n *\n * Use the `useNavItemState(href)` hook to get reactive `isActive` / `isCurrentPage` values.\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useNavItemState } from '@veiag/payload-enhanced-sidebar'\n * import type { CustomNavItemProps } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyNavItem: React.FC<CustomNavItemProps> = ({ entity, href, id, label, badgeConfig }) => {\n * const { isActive, isCurrentPage } = useNavItemState(href)\n * return <a href={href}>{label}</a>\n * }\n * ```\n */\nexport type CustomNavItemProps = {\n /** Badge configuration as defined in the plugin config */\n badgeConfig?: BadgeConfig\n /** The entity (collection, global, or custom item) */\n entity: ExtendedEntity\n /** Computed href with admin route prefix applied */\n href: string\n /** DOM element id */\n id: string\n /** Pre-translated label string */\n label: string\n}\n\n/**\n * Props received by a custom NavGroup component registered via `customComponents.NavGroup`.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomNavGroupProps } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyNavGroup: React.FC<CustomNavGroupProps> = ({ label, isOpen, children }) => {\n * return <div><strong>{label}</strong>{children}</div>\n * }\n * ```\n */\nexport type CustomNavGroupProps = {\n /** Nav items inside the group */\n children: ReactNode\n /** Initial open state from nav preferences */\n isOpen?: boolean\n /** Translated group label */\n label: string\n}\n\n/**\n * Props received by a custom tab icon component set via `iconComponent` on a tab or link.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomTabIconProps } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyIcon: React.FC<CustomTabIconProps> = ({ id, label }) => (\n * <img alt={label} src={`/icons/${id}.svg`} width={20} height={20} />\n * )\n * ```\n */\nexport type CustomTabIconProps = {\n /** Tab/link id */\n id: string\n /** Pre-translated label */\n label: string\n /** Whether this item is a tab or a link */\n type: 'link' | 'tab'\n}\n\n/**\n * Props received by a custom TabButton component registered via `customComponents.TabButton`.\n * Used for both `tab` and `link` type items in the tabs bar.\n *\n * Use `useTabState(id)` for tab active state, or `usePathname()` for link active state.\n * Use `useEnhancedSidebar().onTabChange` to trigger tab switches.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomTabButtonProps } from '@veiag/payload-enhanced-sidebar'\n * import { useTabState, useEnhancedSidebar } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyTabButton: React.FC<CustomTabButtonProps> = ({ id, type, icon, label, href }) => {\n * const { isActive } = useTabState(id)\n * const { onTabChange } = useEnhancedSidebar()\n * if (type === 'link') return <a href={href}>{icon}{label}</a>\n * return <button onClick={() => onTabChange(id)}>{icon}{label}</button>\n * }\n * ```\n */\nexport type CustomTabButtonProps = {\n /** Badge configuration as defined in the plugin config */\n badge?: BadgeConfig\n /** Computed href (for link type, with admin route prefix applied) */\n href?: string\n /** Pre-rendered icon — either from `iconComponent` or the default Lucide icon */\n icon: ReactNode\n /** Tab/link id */\n id: string\n /** Whether the link is external (only set for link type) */\n isExternal?: boolean\n /** Pre-translated label */\n label: string\n /** Item type — use to differentiate rendering */\n type: 'link' | 'tab'\n}\n\n/**\n * Props received by a custom NavContent component registered via `customComponents.NavContent`.\n *\n * Renders in place of the default `<nav>` content area. Use the `useTabState(id)` hook\n * to determine which tab is active and show/hide content accordingly.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomNavContentProps } from '@veiag/payload-enhanced-sidebar'\n * import { useTabState } from '@veiag/payload-enhanced-sidebar'\n *\n * const TabPanel = ({ id, content }: { id: string; content: React.ReactNode }) => {\n * const { isActive } = useTabState(id)\n * return <div style={{ display: isActive ? undefined : 'none' }}>{content}</div>\n * }\n *\n * export const MyNavContent: React.FC<CustomNavContentProps> = ({\n * tabs, tabsContent, beforeNavLinks, afterNavLinks,\n * }) => {\n * return (\n * <nav>\n * {beforeNavLinks}\n * {tabs.map(tab => <TabPanel key={tab.id} id={tab.id} content={tabsContent[tab.id]} />)}\n * {afterNavLinks}\n * </nav>\n * )\n * }\n * ```\n */\nexport type CustomNavContentProps = {\n /** Rendered afterNavLinks from payload config */\n afterNavLinks?: ReactNode\n /** Content to show when no tabs are defined */\n allContent?: ReactNode\n /** Rendered beforeNavLinks from payload config */\n beforeNavLinks?: ReactNode\n /** Tab definitions (id only) for mapping over */\n tabs: Array<{ id: string }>\n /** Pre-rendered content per tab id */\n tabsContent: Record<string, ReactNode>\n}\n\n/**\n * Configuration for the enhanced sidebar\n */\nexport interface EnhancedSidebarConfig {\n /**\n * Badge configurations for sidebar items (collections, globals, custom items).\n * Key is the slug of the item.\n *\n * @example\n * ```typescript\n * badges: {\n * 'posts': { type: 'collection-count', color: 'primary' },\n * 'orders': { type: 'api', endpoint: '/api/orders/pending', responseKey: 'count' },\n * 'notifications': { type: 'provider', color: 'error' },\n * }\n * ```\n */\n badges?: Record<string, BadgeConfig>\n\n /**\n * Custom components to replace the default NavItem, NavGroup, and/or NavContent rendering.\n * Each field accepts a plain path string or a `SidebarComponent` object `{ path, clientProps }`\n * to forward additional props. The plugin registers them in the import map automatically.\n *\n * @example\n * ```typescript\n * customComponents: {\n * // Plain path\n * NavItem: './components/MySidebar#MyNavItem',\n * // With extra client props forwarded to the component\n * NavGroup: { path: './components/MySidebar#MyNavGroup', clientProps: { collapsible: true } },\n * }\n * ```\n */\n customComponents?: {\n /**\n * Custom NavContent component. Replaces the default `<nav>` content area.\n * Receives `CustomNavContentProps`. Use `useTabState(id)` to check if a tab is active.\n */\n NavContent?: SidebarComponent\n /** Custom NavGroup component. Receives `CustomNavGroupProps`. */\n NavGroup?: SidebarComponent\n /** Custom NavItem component. Receives `CustomNavItemProps`. */\n NavItem?: SidebarComponent\n /**\n * Custom tab button component. Used for both `tab` and `link` items in the tabs bar.\n * Receives `CustomTabButtonProps`. Use `useTabState(id)` and `useEnhancedSidebar()` for state.\n */\n TabButton?: SidebarComponent\n }\n\n /**\n * Disable the plugin\n * @default false\n */\n disabled?: boolean\n\n /**\n * Custom icons for collections and globals in the default tab.\n */\n icons?: {\n collections?: Partial<Record<CollectionSlug, IconName>>\n globals?: Partial<Record<GlobalSlug, IconName>>\n }\n\n /**\n * Show logout button at the bottom of the tabs bar.\n * @default true\n */\n showLogout?: boolean\n\n /**\n * Tabs and links to show in the sidebar tabs bar.\n * Order matters - items are rendered top to bottom.\n *\n * @default [{ type: 'tab', id: 'default', icon: 'LayoutGrid', label: 'Collections' }]\n */\n tabs?: SidebarTab[]\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\ninterface BaseExtendedEntity {\n label: Record<string, string> | string\n slug: string\n type: 'collection' | 'custom' | 'global'\n}\n\ninterface InternalExtendedEntity extends BaseExtendedEntity {\n href?: '' | `/${string}`\n isExternal?: false\n}\n\ninterface ExternalExtendedEntity extends BaseExtendedEntity {\n href: string\n isExternal: true\n}\n\nexport type ExtendedEntity = ExternalExtendedEntity | InternalExtendedEntity\n\nexport type ExtendedGroup = {\n entities: ExtendedEntity[]\n label: Record<string, string> | string\n}\n"],"names":[],"mappings":"AAwgBA,WAGC"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { icons, LucideIcon } from 'lucide-react'\nimport type { CollectionSlug, GlobalSlug, PayloadRequest, Where } from 'payload'\nimport type { ReactNode } from 'react'\n\nexport type IconName = keyof typeof icons\n\nexport type LocalizedString = { [locale: string]: string } | string\n\nexport type InternalIcon = IconName | LucideIcon\n\n// ============================================\n// Badge Types\n// ============================================\n\n/**\n * Available badge color variants\n */\nexport type BadgeColor = 'default' | 'error' | 'primary' | 'success' | 'warning'\n\n/**\n * Badge configuration Trr API-based fetching\n */\nexport interface BadgeConfigApi {\n /**\n * Badge color variant\n * @default 'default'\n */\n color?: BadgeColor\n /**\n * API endpoint to fetch badge data from.\n * Can be relative (to current origin) or absolute URL.\n */\n endpoint: string\n /**\n * HTTP method for the request\n * @default 'GET'\n */\n method?: 'GET' | 'POST'\n /**\n * Key in the response object to extract the count from\n * @default 'count'\n */\n responseKey?: string\n type: 'api'\n}\n\n/**\n * Badge configuration for provider-based values.\n * Values are provided via BadgeProvider context.\n */\nexport interface BadgeConfigProvider {\n /**\n * Badge color variant\n * @default 'default'\n */\n color?: BadgeColor\n /**\n * Slug to look up in the BadgeProvider values.\n * If not specified, defaults to the item's id/slug.\n */\n slug?: string\n type: 'provider'\n}\n\n/**\n * Badge configuration for automatic collection document count.\n * Fetches from /api/{collectionSlug}?limit=0 and uses totalDocs.\n */\nexport interface BadgeConfigCollectionCount {\n /**\n * Collection slug to count documents from.\n * If not specified, defaults to the item's slug.\n */\n collectionSlug?: CollectionSlug\n /**\n * Badge color variant\n * @default 'default'\n */\n color?: BadgeColor\n type: 'collection-count'\n /**\n * Optional where query to filter documents.\n * Will be serialized as query string.\n * @example { status: { equals: 'draft' } }\n */\n where?: Where\n}\n\n/**\n * Badge configuration - union of all badge types\n */\nexport type BadgeConfig = BadgeConfigApi | BadgeConfigCollectionCount | BadgeConfigProvider\n\n/**\n * Badge values provided via BadgeProvider context\n */\nexport type BadgeValues = Record<string, number | ReactNode>\n\n// ============================================\n// Access Control Types\n// ============================================\n\n/**\n * Access function for a tab or link in the tabs bar.\n * Return `false` to hide the item entirely (both the button and its content).\n */\nexport type TabAccessFunction = (args: {\n item: SidebarTab\n req: PayloadRequest\n}) => boolean | Promise<boolean>\n\n/**\n * Access function for a custom item inside a tab.\n * Return `false` to hide the item from the nav.\n */\nexport type ItemAccessFunction = (args: {\n item: SidebarTabItem\n req: PayloadRequest\n}) => boolean | Promise<boolean>\n\n// ============================================\n// Enhanced Sidebar Types\n// ============================================\n\n/**\n * Path to a component, either as a plain string or as an object with a path and\n * additional client props to forward to the component. Follows Payload's component format.\n *\n * @example\n * ```typescript\n * // Simple path\n * iconComponent: './components/Icons#TabIcon'\n *\n * // With client props — useful for a single component shared across all tabs\n * iconComponent: {\n * path: './components/Icons#TabIcon',\n * clientProps: { icon: 'house' },\n * }\n * ```\n */\nexport type SidebarComponent =\n | {\n /** Additional props forwarded to the component on the client */\n clientProps?: Record<string, unknown>\n /** Component path in Payload's format: `'./path/to/file#ExportName'` */\n path: string\n }\n | string\n\n/**\n * Mutually exclusive icon config: either a Lucide icon name OR a custom icon component path.\n */\ntype TabIconConfig =\n | {\n /** Icon name from lucide-react */\n icon: IconName\n iconComponent?: never\n }\n | {\n icon?: never\n /**\n * Path to a custom icon component. Receives `CustomTabIconProps`.\n * Supports a plain string path or `{ path, clientProps }` to forward extra props.\n * Registered automatically in the import map.\n */\n iconComponent: SidebarComponent\n }\n\n/**\n * Sidebar tab that shows content when selected\n */\nexport type SidebarTabContent = {\n /**\n * Access control function. Called server-side with the current request.\n * Return `false` to hide this tab (button + content) entirely.\n */\n access?: TabAccessFunction\n /**\n * Badge configuration for this tab.\n * Shows a badge on the tab icon in the tabs bar.\n */\n badge?: BadgeConfig\n /**\n * Collections to show in this tab.\n * If not specified, no collections are shown (unless items are specified).\n * Use collection slugs.\n */\n collections?: CollectionSlug[]\n /**\n * Custom items to add to this tab.\n * Items with `group` will be merged into matching collection groups.\n * Items without `group` will be shown at the bottom as a flat list.\n */\n customItems?: SidebarTabItem[]\n /**\n * Globals to show in this tab.\n * If not specified, no globals are shown.\n * Use global slugs.\n */\n globals?: GlobalSlug[]\n /** Unique identifier for the tab */\n id: string\n /** Tooltip/label for the tab */\n label: LocalizedString\n type: 'tab'\n} & TabIconConfig\n\ntype SidebarTabLinkBase = {\n /**\n * Access control function. Called server-side with the current request.\n * Return `false` to hide this link entirely.\n */\n access?: TabAccessFunction\n /**\n * Badge configuration for this link.\n * Shows a badge on the link icon in the tabs bar.\n */\n badge?: BadgeConfig\n /** Unique identifier */\n id: string\n /** Tooltip/label */\n label: LocalizedString\n type: 'link'\n} & TabIconConfig\ntype SidebarTabLinkExternal = {\n /** Link href (absolute URL) */\n href: string\n isExternal: true\n} & SidebarTabLinkBase\ntype SidebarTabLinkInternal = {\n /** Link href (relative to admin route) */\n href: '' | `/${string}`\n isExternal?: false\n} & SidebarTabLinkBase\n/**\n * Sidebar link that navigates to a URL (not a tab)\n */\nexport type SidebarTabLink = SidebarTabLinkExternal | SidebarTabLinkInternal\n/**\n * A tab or link in the sidebar tabs bar\n */\nexport type SidebarTab = SidebarTabContent | SidebarTabLink\n\ninterface BaseSidebarTabItem {\n /**\n * Access control function. Called server-side with the current request.\n * Return `false` to hide this item from the nav.\n */\n access?: ItemAccessFunction\n /**\n * Group to add this item to.\n * If matches an existing collection group label, item will be merged into that group.\n * If no match found, a new group will be created with this label.\n * If not specified, item will be shown as ungrouped (position controlled by `position`).\n */\n group?: LocalizedString\n /** Display label */\n label: LocalizedString\n /**\n * Where to place this item relative to collection groups in the tab.\n * - `'top'` — appears above all collection/global groups\n * - `'bottom'` — appears below all groups (default)\n *\n * Has no effect on items that are merged into an existing collection group via `group`.\n * For new custom groups (unmatched `group` label), controls whether the group appears\n * at the top or bottom of the nav.\n *\n * @default 'bottom'\n */\n position?: 'bottom' | 'top'\n /** Unique slug for the item */\n slug: string\n}\ninterface ExternalHrefItem extends BaseSidebarTabItem {\n /** Link href (absolute URL or relative to root) */\n href: string\n /** Whether the link is external, without admin route prefix. */\n isExternal: true\n}\n\ninterface InternalHrefItem extends BaseSidebarTabItem {\n /** Link href (relative to admin route) */\n href: '' | `/${string}`\n /** Whether the link is external, without admin route prefix. */\n isExternal?: false\n}\n/**\n * Custom item inside a sidebar tab\n */\nexport type SidebarTabItem = ExternalHrefItem | InternalHrefItem\n\n// ============================================\n// Custom Component Types\n// ============================================\n\n/**\n * Props received by a custom NavItem component registered via `customComponents.NavItem`.\n *\n * Use the `useNavItemState(href)` hook to get reactive `isActive` / `isCurrentPage` values.\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useNavItemState } from '@veiag/payload-enhanced-sidebar'\n * import type { CustomNavItemProps } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyNavItem: React.FC<CustomNavItemProps> = ({ entity, href, id, label, badgeConfig }) => {\n * const { isActive, isCurrentPage } = useNavItemState(href)\n * return <a href={href}>{label}</a>\n * }\n * ```\n */\nexport type CustomNavItemProps = {\n /** Badge configuration as defined in the plugin config */\n badgeConfig?: BadgeConfig\n /** The entity (collection, global, or custom item) */\n entity: ExtendedEntity\n /** Computed href with admin route prefix applied */\n href: string\n /** DOM element id */\n id: string\n /** Pre-translated label string */\n label: string\n}\n\n/**\n * Props received by a custom NavGroup component registered via `customComponents.NavGroup`.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomNavGroupProps } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyNavGroup: React.FC<CustomNavGroupProps> = ({ label, isOpen, children }) => {\n * return <div><strong>{label}</strong>{children}</div>\n * }\n * ```\n */\nexport type CustomNavGroupProps = {\n /** Nav items inside the group */\n children: ReactNode\n /** Initial open state from nav preferences */\n isOpen?: boolean\n /** Translated group label */\n label: string\n}\n\n/**\n * Props received by a custom tab icon component set via `iconComponent` on a tab or link.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomTabIconProps } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyIcon: React.FC<CustomTabIconProps> = ({ id, label }) => (\n * <img alt={label} src={`/icons/${id}.svg`} width={20} height={20} />\n * )\n * ```\n */\nexport type CustomTabIconProps = {\n /** Tab/link id */\n id: string\n /** Pre-translated label */\n label: string\n /** Whether this item is a tab or a link */\n type: 'link' | 'tab'\n}\n\n/**\n * Props received by a custom TabButton component registered via `customComponents.TabButton`.\n * Used for both `tab` and `link` type items in the tabs bar.\n *\n * Use `useTabState(id)` for tab active state, or `usePathname()` for link active state.\n * Use `useEnhancedSidebar().onTabChange` to trigger tab switches.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomTabButtonProps } from '@veiag/payload-enhanced-sidebar'\n * import { useTabState, useEnhancedSidebar } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyTabButton: React.FC<CustomTabButtonProps> = ({ id, type, icon, label, href }) => {\n * const { isActive } = useTabState(id)\n * const { onTabChange } = useEnhancedSidebar()\n * if (type === 'link') return <a href={href}>{icon}{label}</a>\n * return <button onClick={() => onTabChange(id)}>{icon}{label}</button>\n * }\n * ```\n */\nexport type CustomTabButtonProps = {\n /** Badge configuration as defined in the plugin config */\n badge?: BadgeConfig\n /** Computed href (for link type, with admin route prefix applied) */\n href?: string\n /** Pre-rendered icon — either from `iconComponent` or the default Lucide icon */\n icon: ReactNode\n /** Tab/link id */\n id: string\n /** Whether the link is external (only set for link type) */\n isExternal?: boolean\n /** Pre-translated label */\n label: string\n /** Item type — use to differentiate rendering */\n type: 'link' | 'tab'\n}\n\n/**\n * Props received by a custom NavContent component registered via `customComponents.NavContent`.\n *\n * Renders in place of the default `<nav>` content area. Use the `useTabState(id)` hook\n * to determine which tab is active and show/hide content accordingly.\n *\n * @example\n * ```tsx\n * 'use client'\n * import type { CustomNavContentProps } from '@veiag/payload-enhanced-sidebar'\n * import { useTabState } from '@veiag/payload-enhanced-sidebar'\n *\n * const TabPanel = ({ id, content }: { id: string; content: React.ReactNode }) => {\n * const { isActive } = useTabState(id)\n * return <div style={{ display: isActive ? undefined : 'none' }}>{content}</div>\n * }\n *\n * export const MyNavContent: React.FC<CustomNavContentProps> = ({\n * tabs, tabsContent, beforeNavLinks, afterNavLinks,\n * }) => {\n * return (\n * <nav>\n * {beforeNavLinks}\n * {tabs.map(tab => <TabPanel key={tab.id} id={tab.id} content={tabsContent[tab.id]} />)}\n * {afterNavLinks}\n * </nav>\n * )\n * }\n * ```\n */\nexport type CustomNavContentProps = {\n /** Rendered afterNavLinks from payload config */\n afterNavLinks?: ReactNode\n /** Content to show when no tabs are defined */\n allContent?: ReactNode\n /** Rendered beforeNavLinks from payload config */\n beforeNavLinks?: ReactNode\n /** Tab definitions (id only) for mapping over */\n tabs: Array<{ id: string }>\n /** Pre-rendered content per tab id */\n tabsContent: Record<string, ReactNode>\n}\n\n/**\n * Configuration for the enhanced sidebar\n */\nexport interface EnhancedSidebarConfig {\n /**\n * Badge configurations for sidebar items (collections, globals, custom items).\n * Key is the slug of the item.\n *\n * @example\n * ```typescript\n * badges: {\n * 'posts': { type: 'collection-count', color: 'primary' },\n * 'orders': { type: 'api', endpoint: '/api/orders/pending', responseKey: 'count' },\n * 'notifications': { type: 'provider', color: 'error' },\n * }\n * ```\n */\n badges?: Record<string, BadgeConfig>\n\n /**\n * Custom components to replace the default NavItem, NavGroup, and/or NavContent rendering.\n * Each field accepts a plain path string or a `SidebarComponent` object `{ path, clientProps }`\n * to forward additional props. The plugin registers them in the import map automatically.\n *\n * @example\n * ```typescript\n * customComponents: {\n * // Plain path\n * NavItem: './components/MySidebar#MyNavItem',\n * // With extra client props forwarded to the component\n * NavGroup: { path: './components/MySidebar#MyNavGroup', clientProps: { collapsible: true } },\n * }\n * ```\n */\n customComponents?: {\n /**\n * Custom NavContent component. Replaces the default `<nav>` content area.\n * Receives `CustomNavContentProps`. Use `useTabState(id)` to check if a tab is active.\n */\n NavContent?: SidebarComponent\n /** Custom NavGroup component. Receives `CustomNavGroupProps`. */\n NavGroup?: SidebarComponent\n /** Custom NavItem component. Receives `CustomNavItemProps`. */\n NavItem?: SidebarComponent\n /**\n * Custom tab button component. Used for both `tab` and `link` items in the tabs bar.\n * Receives `CustomTabButtonProps`. Use `useTabState(id)` and `useEnhancedSidebar()` for state.\n */\n TabButton?: SidebarComponent\n }\n\n /**\n * Disable the plugin\n * @default false\n */\n disabled?: boolean\n\n /**\n * Custom icons for collections and globals in the default tab.\n */\n icons?: {\n collections?: Partial<Record<CollectionSlug, IconName>>\n globals?: Partial<Record<GlobalSlug, IconName>>\n }\n\n /**\n * Show logout button at the bottom of the tabs bar.\n * @default true\n */\n showLogout?: boolean\n\n /**\n * Tabs and links to show in the sidebar tabs bar.\n * Order matters - items are rendered top to bottom.\n *\n * @default [{ type: 'tab', id: 'default', icon: 'LayoutGrid', label: 'Collections' }]\n */\n tabs?: SidebarTab[]\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\ninterface BaseExtendedEntity {\n label: Record<string, string> | string\n slug: string\n type: 'collection' | 'custom' | 'global'\n}\n\ninterface InternalExtendedEntity extends BaseExtendedEntity {\n href?: '' | `/${string}`\n isExternal?: false\n}\n\ninterface ExternalExtendedEntity extends BaseExtendedEntity {\n href: string\n isExternal: true\n}\n\nexport type ExtendedEntity = ExternalExtendedEntity | InternalExtendedEntity\n\nexport type ExtendedGroup = {\n entities: ExtendedEntity[]\n label: Record<string, string> | string\n}\n"],"names":[],"mappings":"AA6iBA,WAGC"}
@@ -1,4 +1,4 @@
1
- import type { LocalizedString, SidebarComponent } from '../types';
1
+ import type { EnhancedSidebarConfig, LocalizedString, SidebarComponent } from '../types';
2
2
  export declare const convertSlugToTitle: (slug: string) => string;
3
3
  /**
4
4
  * Extracts path and clientProps from a SidebarComponent (string or object).
@@ -7,4 +7,9 @@ export declare const resolveSidebarComponent: (component: SidebarComponent) => {
7
7
  clientProps: Record<string, unknown>;
8
8
  path: string;
9
9
  };
10
+ /**
11
+ * Strips all non-serializable values (functions) from the sidebar config
12
+ * before passing it to client components.
13
+ */
14
+ export declare const sanitizeSidebarConfig: (config: EnhancedSidebarConfig) => EnhancedSidebarConfig;
10
15
  export declare const extractLocalizedValue: (value: LocalizedString | undefined, locale: string, fallbackSlug?: string) => string;
@@ -15,6 +15,22 @@ export const convertSlugToTitle = (slug)=>{
15
15
  path: component.path
16
16
  };
17
17
  };
18
+ /**
19
+ * Strips all non-serializable values (functions) from the sidebar config
20
+ * before passing it to client components.
21
+ */ export const sanitizeSidebarConfig = (config)=>({
22
+ ...config,
23
+ tabs: config.tabs?.map((tab)=>{
24
+ const { access: _, ...tabRest } = tab;
25
+ if (tabRest.type === 'tab' && tabRest.customItems) {
26
+ return {
27
+ ...tabRest,
28
+ customItems: tabRest.customItems.map(({ access: __, ...item })=>item)
29
+ };
30
+ }
31
+ return tabRest;
32
+ })
33
+ });
18
34
  export const extractLocalizedValue = (value, locale, fallbackSlug)=>{
19
35
  if (!value) {
20
36
  return fallbackSlug ? convertSlugToTitle(fallbackSlug) : '';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/index.ts"],"sourcesContent":["import type { LocalizedString, SidebarComponent } from '../types'\n\nexport const convertSlugToTitle = (slug: string): string => {\n return slug.replace(/-/g, ' ').replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\n/**\n * Extracts path and clientProps from a SidebarComponent (string or object).\n */\nexport const resolveSidebarComponent = (\n component: SidebarComponent,\n): { clientProps: Record<string, unknown>; path: string } => {\n if (typeof component === 'string') {\n return { clientProps: {}, path: component }\n }\n return { clientProps: component.clientProps ?? {}, path: component.path }\n}\n\nexport const extractLocalizedValue = (\n value: LocalizedString | undefined,\n locale: string,\n fallbackSlug?: string,\n): string => {\n if (!value) {\n return fallbackSlug ? convertSlugToTitle(fallbackSlug) : ''\n }\n if (typeof value === 'string') {\n return value\n }\n return value[locale] || Object.values(value)[0] || (fallbackSlug ? convertSlugToTitle(fallbackSlug) : '')\n}\n"],"names":["convertSlugToTitle","slug","replace","char","toUpperCase","resolveSidebarComponent","component","clientProps","path","extractLocalizedValue","value","locale","fallbackSlug","Object","values"],"mappings":"AAEA,OAAO,MAAMA,qBAAqB,CAACC;IACjC,OAAOA,KAAKC,OAAO,CAAC,MAAM,KAAKA,OAAO,CAAC,SAAS,CAACC,OAASA,KAAKC,WAAW;AAC5E,EAAC;AAED;;CAEC,GACD,OAAO,MAAMC,0BAA0B,CACrCC;IAEA,IAAI,OAAOA,cAAc,UAAU;QACjC,OAAO;YAAEC,aAAa,CAAC;YAAGC,MAAMF;QAAU;IAC5C;IACA,OAAO;QAAEC,aAAaD,UAAUC,WAAW,IAAI,CAAC;QAAGC,MAAMF,UAAUE,IAAI;IAAC;AAC1E,EAAC;AAED,OAAO,MAAMC,wBAAwB,CACnCC,OACAC,QACAC;IAEA,IAAI,CAACF,OAAO;QACV,OAAOE,eAAeZ,mBAAmBY,gBAAgB;IAC3D;IACA,IAAI,OAAOF,UAAU,UAAU;QAC7B,OAAOA;IACT;IACA,OAAOA,KAAK,CAACC,OAAO,IAAIE,OAAOC,MAAM,CAACJ,MAAM,CAAC,EAAE,IAAKE,CAAAA,eAAeZ,mBAAmBY,gBAAgB,EAAC;AACzG,EAAC"}
1
+ {"version":3,"sources":["../../src/utils/index.ts"],"sourcesContent":["import type { EnhancedSidebarConfig, LocalizedString, SidebarComponent } from '../types'\n\nexport const convertSlugToTitle = (slug: string): string => {\n return slug.replace(/-/g, ' ').replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\n/**\n * Extracts path and clientProps from a SidebarComponent (string or object).\n */\nexport const resolveSidebarComponent = (\n component: SidebarComponent,\n): { clientProps: Record<string, unknown>; path: string } => {\n if (typeof component === 'string') {\n return { clientProps: {}, path: component }\n }\n return { clientProps: component.clientProps ?? {}, path: component.path }\n}\n\n/**\n * Strips all non-serializable values (functions) from the sidebar config\n * before passing it to client components.\n */\nexport const sanitizeSidebarConfig = (config: EnhancedSidebarConfig): EnhancedSidebarConfig => ({\n ...config,\n tabs: config.tabs?.map((tab) => {\n const { access: _, ...tabRest } = tab\n if (tabRest.type === 'tab' && tabRest.customItems) {\n return {\n ...tabRest,\n customItems: tabRest.customItems.map(({ access: __, ...item }) => item),\n }\n }\n return tabRest\n }),\n})\n\nexport const extractLocalizedValue = (\n value: LocalizedString | undefined,\n locale: string,\n fallbackSlug?: string,\n): string => {\n if (!value) {\n return fallbackSlug ? convertSlugToTitle(fallbackSlug) : ''\n }\n if (typeof value === 'string') {\n return value\n }\n return value[locale] || Object.values(value)[0] || (fallbackSlug ? convertSlugToTitle(fallbackSlug) : '')\n}\n"],"names":["convertSlugToTitle","slug","replace","char","toUpperCase","resolveSidebarComponent","component","clientProps","path","sanitizeSidebarConfig","config","tabs","map","tab","access","_","tabRest","type","customItems","__","item","extractLocalizedValue","value","locale","fallbackSlug","Object","values"],"mappings":"AAEA,OAAO,MAAMA,qBAAqB,CAACC;IACjC,OAAOA,KAAKC,OAAO,CAAC,MAAM,KAAKA,OAAO,CAAC,SAAS,CAACC,OAASA,KAAKC,WAAW;AAC5E,EAAC;AAED;;CAEC,GACD,OAAO,MAAMC,0BAA0B,CACrCC;IAEA,IAAI,OAAOA,cAAc,UAAU;QACjC,OAAO;YAAEC,aAAa,CAAC;YAAGC,MAAMF;QAAU;IAC5C;IACA,OAAO;QAAEC,aAAaD,UAAUC,WAAW,IAAI,CAAC;QAAGC,MAAMF,UAAUE,IAAI;IAAC;AAC1E,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMC,wBAAwB,CAACC,SAA0D,CAAA;QAC9F,GAAGA,MAAM;QACTC,MAAMD,OAAOC,IAAI,EAAEC,IAAI,CAACC;YACtB,MAAM,EAAEC,QAAQC,CAAC,EAAE,GAAGC,SAAS,GAAGH;YAClC,IAAIG,QAAQC,IAAI,KAAK,SAASD,QAAQE,WAAW,EAAE;gBACjD,OAAO;oBACL,GAAGF,OAAO;oBACVE,aAAaF,QAAQE,WAAW,CAACN,GAAG,CAAC,CAAC,EAAEE,QAAQK,EAAE,EAAE,GAAGC,MAAM,GAAKA;gBACpE;YACF;YACA,OAAOJ;QACT;IACF,CAAA,EAAE;AAEF,OAAO,MAAMK,wBAAwB,CACnCC,OACAC,QACAC;IAEA,IAAI,CAACF,OAAO;QACV,OAAOE,eAAexB,mBAAmBwB,gBAAgB;IAC3D;IACA,IAAI,OAAOF,UAAU,UAAU;QAC7B,OAAOA;IACT;IACA,OAAOA,KAAK,CAACC,OAAO,IAAIE,OAAOC,MAAM,CAACJ,MAAM,CAAC,EAAE,IAAKE,CAAAA,eAAexB,mBAAmBwB,gBAAgB,EAAC;AACzG,EAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veiag/payload-enhanced-sidebar",
3
- "version": "0.3.0-beta.0",
3
+ "version": "0.3.0-beta.2",
4
4
  "description": "An enhanced sidebar plugin for Payload CMS with tabbed navigation to organize collections and globals.",
5
5
  "author": "VeiaG",
6
6
  "license": "MIT",