@deenruv/admin-dashboard 1.0.16 → 1.0.17-dev.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/DeenruvAdminPanel.js +35 -46
  2. package/dist/access/access-context.js +10 -0
  3. package/dist/access/built-in-routes.js +300 -0
  4. package/dist/access/index.js +6 -0
  5. package/dist/access/navigation.js +33 -0
  6. package/dist/access/permission-access.js +9 -0
  7. package/dist/access/permission-routes.js +3 -0
  8. package/dist/access/types.js +1 -0
  9. package/dist/components/GlobalSearch.js +37 -62
  10. package/dist/components/Menu/ActiveAdmins.js +1 -1
  11. package/dist/components/Menu/ChannelSwitcher.js +1 -1
  12. package/dist/components/Menu/LanguagesDropdown.js +1 -1
  13. package/dist/components/Menu/Navigation.js +68 -229
  14. package/dist/components/Menu/NavigationFooter.js +7 -4
  15. package/dist/components/Menu/Notifications.js +1 -1
  16. package/dist/components/Menu/index.js +50 -16
  17. package/dist/components/storefrontAction/StorefrontAction.js +9 -0
  18. package/dist/components/storefrontAction/StorefrontAction.logic.js +27 -0
  19. package/dist/components/storefrontAction/index.js +2 -0
  20. package/dist/index.css +54 -44
  21. package/dist/locales/en/admins.json +62 -0
  22. package/dist/locales/en/collections.json +21 -0
  23. package/dist/locales/en/common.json +2 -0
  24. package/dist/locales/en/orders.json +8 -0
  25. package/dist/locales/en/products.json +29 -2
  26. package/dist/locales/en/promotions.json +4 -0
  27. package/dist/locales/pl/admins.json +62 -0
  28. package/dist/locales/pl/collections.json +21 -0
  29. package/dist/locales/pl/common.json +2 -0
  30. package/dist/locales/pl/orders.json +8 -0
  31. package/dist/locales/pl/products.json +29 -2
  32. package/dist/locales/pl/promotions.json +4 -0
  33. package/dist/pages/Root.js +31 -15
  34. package/dist/pages/admins/Detail.js +1 -1
  35. package/dist/pages/admins/List.js +8 -3
  36. package/dist/pages/admins/Provision.js +173 -0
  37. package/dist/pages/admins/Provision.logic.js +53 -0
  38. package/dist/pages/admins/index.js +1 -0
  39. package/dist/pages/assets/List.js +3 -3
  40. package/dist/pages/channels/Detail.js +1 -1
  41. package/dist/pages/collections/Detail.js +24 -6
  42. package/dist/pages/collections/List.js +87 -20
  43. package/dist/pages/collections/_components/CollectionDetailView.js +15 -2
  44. package/dist/pages/collections/_components/FiltersCard.js +51 -11
  45. package/dist/pages/collections/_components/collectionStorefrontAction/CollectionStorefrontAction.js +41 -0
  46. package/dist/pages/collections/_components/collectionStorefrontAction/CollectionStorefrontAction.logic.js +44 -0
  47. package/dist/pages/collections/_components/collectionStorefrontAction/index.js +2 -0
  48. package/dist/pages/countries/Detail.js +1 -1
  49. package/dist/pages/customers/_components/PersonalDataCard.js +2 -1
  50. package/dist/pages/extensions/Extensions.js +3 -7
  51. package/dist/pages/facets/Detail.js +1 -1
  52. package/dist/pages/orders/Detail.js +38 -5
  53. package/dist/pages/orders/List.js +1 -16
  54. package/dist/pages/orders/_components/CancelAndRefundDialog.js +1 -2
  55. package/dist/pages/orders/_components/OrderLineCustomFields.js +1 -1
  56. package/dist/pages/orders/_components/ProductsTable.js +1 -2
  57. package/dist/pages/orders/_components/RefundPaymentCard.js +1 -2
  58. package/dist/pages/payment-methods/Detail.js +1 -1
  59. package/dist/pages/product-variants/List.js +3 -3
  60. package/dist/pages/products/Detail.js +62 -5
  61. package/dist/pages/products/_components/AssetsCard.js +15 -2
  62. package/dist/pages/products/_components/OptionsCard.js +7 -5
  63. package/dist/pages/products/_components/PriceCard.js +21 -8
  64. package/dist/pages/products/_components/ProductDetailView.js +7 -3
  65. package/dist/pages/products/_components/Variant.js +53 -26
  66. package/dist/pages/products/_components/VariantsTab.js +1 -1
  67. package/dist/pages/products/_components/productStorefrontAction/ProductStorefrontAction.js +42 -0
  68. package/dist/pages/products/_components/productStorefrontAction/ProductStorefrontAction.logic.js +39 -0
  69. package/dist/pages/products/_components/productStorefrontAction/index.js +2 -0
  70. package/dist/pages/promotions/Detail.js +34 -3
  71. package/dist/pages/promotions/_components/EnabledCard.js +2 -2
  72. package/dist/pages/promotions/_components/PromotionDetailSidebar.js +2 -1
  73. package/dist/pages/roles/Detail.js +1 -1
  74. package/dist/pages/roles/_components/PermissionsTable.js +0 -1
  75. package/dist/pages/sellers/Detail.js +1 -1
  76. package/dist/pages/shipping-methods/Detail.js +10 -6
  77. package/dist/pages/shipping-methods/_components/CalculatorCard.js +27 -11
  78. package/dist/pages/shipping-methods/_components/CheckerCard.js +28 -12
  79. package/dist/pages/stock-locations/Detail.js +1 -1
  80. package/dist/pages/tax-rates/Detail.js +1 -1
  81. package/dist/pages/zones/Detail.js +1 -1
  82. package/dist/version.js +1 -1
  83. package/package.json +68 -68
@@ -1,16 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, usePluginStore, Routes, useGlobalSearch, capitalizeFirstLetter, useTranslation, } from '@deenruv/react-ui-devkit';
2
+ import { CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, useGlobalSearch, capitalizeFirstLetter, useTranslation, useServer, } from '@deenruv/react-ui-devkit';
3
3
  import { useEffect, useMemo } from 'react';
4
4
  import { useNavigate } from 'react-router';
5
5
  import { LayoutDashboard, ListPlus, FileText, Puzzle, ArrowRight } from 'lucide-react';
6
+ import { canAccessAdminItem, useAdminAccess } from "../access/index.js";
6
7
  export const GlobalSearch = () => {
7
8
  const { t, tEntity } = useTranslation('common');
8
- const { plugins } = usePluginStore();
9
+ const { routes } = useAdminAccess();
10
+ const userPermissions = useServer((p) => p.userPermissions);
9
11
  const isOpen = useGlobalSearch((s) => s.isOpen);
10
12
  const toggle = useGlobalSearch((s) => s.toggle);
11
13
  const close = useGlobalSearch((s) => s.close);
12
14
  const navigate = useNavigate();
15
+ const isEnabled = true;
13
16
  useEffect(() => {
17
+ if (!isEnabled)
18
+ return;
14
19
  const handleKeyDown = (e) => {
15
20
  if ((e.metaKey || e.ctrlKey) && e.key === ' ') {
16
21
  e.preventDefault();
@@ -19,63 +24,33 @@ export const GlobalSearch = () => {
19
24
  };
20
25
  document.addEventListener('keydown', handleKeyDown);
21
26
  return () => document.removeEventListener('keydown', handleKeyDown);
22
- }, [toggle]);
27
+ }, [isEnabled, toggle]);
23
28
  const allRoutes = useMemo(() => {
24
- const routes = [];
25
- Object.entries(Routes).forEach(([key, value]) => {
26
- if (typeof value === 'object' && value !== null) {
27
- const children = [];
28
- if ('new' in value && typeof value.new === 'string') {
29
- children.push({
30
- name: capitalizeFirstLetter(t(`menu.${key}`)),
31
- path: value.new,
32
- type: 'new',
33
- description: tEntity('Utwórz', key),
34
- });
35
- }
36
- if ('list' in value && typeof value.list === 'string') {
37
- children.push({
38
- name: capitalizeFirstLetter(t(`menu.${key}`)),
39
- path: value.list,
40
- type: 'list',
41
- description: tEntity('Zobacz wszystkie', key, 'many'),
42
- });
43
- }
44
- routes.push({ name: key, children });
45
- }
46
- else if (typeof value === 'string') {
47
- routes.push({
48
- name: capitalizeFirstLetter(t(`menu.${key}`)),
49
- path: value,
50
- type: 'default',
51
- description: `${t('globalSearch.navigateTo')} ${t(`menu.${key}`)}`,
52
- });
53
- }
54
- });
55
- plugins.forEach((plugin) => {
56
- plugin.pages?.forEach((page, pageIndex) => {
57
- if (page.path) {
58
- routes.push({
59
- name: `${plugin.name} - ${page.path || 'Page'}`,
60
- path: page.path,
61
- type: 'plugin',
62
- description: t('globalSearch.accessPlugin', { pluginName: plugin.name }),
63
- });
64
- }
65
- });
29
+ return routes
30
+ .filter((route) => route.search)
31
+ .filter((route) => canAccessAdminItem({ item: route, userPermissions }))
32
+ .map((route) => {
33
+ const search = route.search;
34
+ const isPlugin = search.type === 'plugin';
35
+ const translatedName = isPlugin ? search.menuKey : capitalizeFirstLetter(t(`menu.${search.menuKey}`));
36
+ const description = search.type === 'new'
37
+ ? tEntity('Utwórz', search.menuKey)
38
+ : search.type === 'list'
39
+ ? tEntity('Zobacz wszystkie', search.menuKey, 'many')
40
+ : search.type === 'plugin'
41
+ ? t('globalSearch.accessPlugin', { pluginName: search.menuKey })
42
+ : `${t('globalSearch.navigateTo')} ${t(`menu.${search.menuKey}`)}`;
43
+ return {
44
+ id: route.id,
45
+ name: translatedName,
46
+ path: route.path,
47
+ type: search.type,
48
+ description,
49
+ };
66
50
  });
67
- return routes.flatMap((route, routeIndex) => route.children?.length
68
- ? route.children.map((child, childIndex) => ({
69
- ...child,
70
- id: `${routeIndex}-${childIndex}-${child.type}-${route.name}`,
71
- }))
72
- : [
73
- {
74
- ...route,
75
- id: `${routeIndex}-main-${route.name}`,
76
- },
77
- ]);
78
- }, [plugins]);
51
+ }, [routes, t, tEntity, userPermissions]);
52
+ if (!isEnabled)
53
+ return null;
79
54
  // Group routes by type for better organization
80
55
  const groupedRoutes = useMemo(() => {
81
56
  const core = [];
@@ -110,25 +85,25 @@ export const GlobalSearch = () => {
110
85
  return _jsx(LayoutDashboard, { className: "mr-2 h-4 w-4 text-gray-500" });
111
86
  }
112
87
  };
113
- return (_jsxs(CommandDialog, { open: isOpen, onOpenChange: toggle, modal: true, children: [_jsx(CommandInput, { placeholder: t('globalSearch.placeholder'), className: "flex-1 bg-transparent text-base outline-none placeholder:text-muted-foreground" }), _jsxs(CommandList, { className: "max-h-[300px] overflow-y-auto py-2", children: [_jsx(CommandEmpty, { className: "py-6 text-center text-sm", children: t('globalSearch.emptyState') }), groupedRoutes.core.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.navigation'), className: "px-2", children: groupedRoutes.core.map((route) => (_jsxs(CommandItem, { onSelect: () => {
88
+ return (_jsxs(CommandDialog, { open: isOpen, onOpenChange: toggle, modal: true, children: [_jsx(CommandInput, { placeholder: t('globalSearch.placeholder'), className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" }), _jsxs(CommandList, { className: "max-h-[320px] overflow-y-auto py-2", children: [_jsx(CommandEmpty, { className: "py-6 text-center text-sm", children: t('globalSearch.emptyState') }), groupedRoutes.core.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.navigation'), className: "px-2", children: groupedRoutes.core.map((route) => (_jsxs(CommandItem, { onSelect: () => {
114
89
  if (route.path) {
115
90
  navigate(route.path);
116
91
  }
117
92
  close();
118
- }, className: "group flex cursor-pointer items-center rounded-md px-2 py-2 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `core-${route.name}-${route.path}`))) })), groupedRoutes.newItems.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.createNew'), className: "px-2", children: groupedRoutes.newItems.map((route) => (_jsxs(CommandItem, { onSelect: () => {
93
+ }, className: "group flex cursor-pointer items-center px-3 py-2.5 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `core-${route.name}-${route.path}`))) })), groupedRoutes.newItems.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.createNew'), className: "px-2", children: groupedRoutes.newItems.map((route) => (_jsxs(CommandItem, { onSelect: () => {
119
94
  if (route.path) {
120
95
  navigate(route.path);
121
96
  }
122
97
  close();
123
- }, className: "group flex cursor-pointer items-center rounded-md px-2 py-2 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `new-${route.name}-${route.path}`))) })), groupedRoutes.lists.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.viewLists'), className: "px-2", children: groupedRoutes.lists.map((route) => (_jsxs(CommandItem, { onSelect: () => {
98
+ }, className: "group flex cursor-pointer items-center px-3 py-2.5 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `new-${route.name}-${route.path}`))) })), groupedRoutes.lists.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.viewLists'), className: "px-2", children: groupedRoutes.lists.map((route) => (_jsxs(CommandItem, { onSelect: () => {
124
99
  if (route.path) {
125
100
  navigate(route.path);
126
101
  }
127
102
  close();
128
- }, className: "group flex cursor-pointer items-center rounded-md px-2 py-2 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `list-${route.name}-${route.path}`))) })), groupedRoutes.pluginRoutes.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.plugins'), className: "px-2", children: groupedRoutes.pluginRoutes.map((route) => (_jsxs(CommandItem, { onSelect: () => {
103
+ }, className: "group flex cursor-pointer items-center px-3 py-2.5 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `list-${route.name}-${route.path}`))) })), groupedRoutes.pluginRoutes.length > 0 && (_jsx(CommandGroup, { heading: t('globalSearch.plugins'), className: "px-2", children: groupedRoutes.pluginRoutes.map((route) => (_jsxs(CommandItem, { onSelect: () => {
129
104
  if (route.path) {
130
105
  navigate(route.path);
131
106
  }
132
107
  close();
133
- }, className: "group flex cursor-pointer items-center rounded-md px-2 py-2 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `plugin-${route.name}-${route.path}`))) }))] }), _jsx("div", { className: "border-t bg-muted/50 p-2", children: _jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [_jsxs("div", { className: "flex gap-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("kbd", { className: "rounded border bg-muted px-1.5 py-0.5 text-[10px]", children: "\u2191" }), _jsx("kbd", { className: "rounded border bg-muted px-1.5 py-0.5 text-[10px]", children: "\u2193" }), _jsx("span", { children: t('globalSearch.navigate') })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("kbd", { className: "rounded border bg-muted px-1.5 py-0.5 text-[10px]", children: "Enter" }), _jsx("span", { children: t('globalSearch.select') })] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("kbd", { className: "rounded border bg-muted px-1.5 py-0.5 text-[10px]", children: "Esc" }), _jsx("span", { children: t('globalSearch.close') })] })] }) })] }));
108
+ }, className: "group flex cursor-pointer items-center px-3 py-2.5 transition-colors hover:bg-accent", children: [getRouteIcon(route.type), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: route.name }), route.description && _jsx("span", { className: "text-xs text-muted-foreground", children: route.description })] }), _jsx(ArrowRight, { className: "ml-auto h-4 w-4 opacity-0 transition-opacity group-hover:opacity-70" })] }, route.id || `plugin-${route.name}-${route.path}`))) }))] }), _jsx("div", { className: "border-t bg-card p-2", children: _jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [_jsxs("div", { className: "flex gap-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("kbd", { className: "border bg-muted px-1.5 py-0.5 text-[10px]", children: "\u2191" }), _jsx("kbd", { className: "border bg-muted px-1.5 py-0.5 text-[10px]", children: "\u2193" }), _jsx("span", { children: t('globalSearch.navigate') })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("kbd", { className: "border bg-muted px-1.5 py-0.5 text-[10px]", children: "Enter" }), _jsx("span", { children: t('globalSearch.select') })] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("kbd", { className: "border bg-muted px-1.5 py-0.5 text-[10px]", children: "Esc" }), _jsx("span", { children: t('globalSearch.close') })] })] }) })] }));
134
109
  };
@@ -20,5 +20,5 @@ export const ActiveAdmins = () => {
20
20
  else
21
21
  return t('awesomeMenu.above5MinAgo');
22
22
  };
23
- return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "sm", className: "h-10", children: t('awesomeMenu.activeAdministratorsValue', { value: activeClients.length }) }) }), _jsx(PopoverContent, { className: "mr-4", children: _jsxs("div", { className: "flex flex-col gap-4 rounded-md", children: [_jsx(Label, { className: "select-none", children: t('awesomeMenu.activeAdministrators') }), activeClients.length ? (activeClients.map((client) => (_jsx("div", { className: "flex items-center justify-between", children: _jsxs("div", { className: "flex w-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-bold", children: [client.firstName, " ", client.lastName, " ", client.me && '(me)'] }), _jsx(motion.div, { animate: { scale: [1, 1.2, 1] }, transition: { duration: 1.5, repeat: Infinity }, className: "size-2 rounded-full bg-green-500" })] }), _jsx("span", { className: "text-sm text-muted-foreground", children: formatTimeAgo(client.lastActive) }), !client.me && (_jsx(NavLink, { to: client.location, className: "text-sm text-muted-foreground", viewTransition: true, children: client.location.replace(window.location.origin, '') }))] }) }, client.id)))) : (_jsx("div", { children: _jsx("span", { className: "text-sm text-muted-foreground", children: t('awesomeMenu.noActiveAdministrators') }) }))] }) })] }));
23
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "sm", className: "h-10", children: t('awesomeMenu.activeAdministratorsValue', { value: activeClients.length }) }) }), _jsx(PopoverContent, { className: "mr-4", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Label, { className: "select-none", children: t('awesomeMenu.activeAdministrators') }), activeClients.length ? (activeClients.map((client) => (_jsx("div", { className: "flex items-center justify-between", children: _jsxs("div", { className: "flex w-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-bold", children: [client.firstName, " ", client.lastName, " ", client.me && '(me)'] }), _jsx(motion.div, { animate: { scale: [1, 1.2, 1] }, transition: { duration: 1.5, repeat: Infinity }, className: "size-2 rounded-full bg-green-500" })] }), _jsx("span", { className: "text-sm text-muted-foreground", children: formatTimeAgo(client.lastActive) }), !client.me && (_jsx(NavLink, { to: client.location, className: "text-sm text-muted-foreground", viewTransition: true, children: client.location.replace(window.location.origin, '') }))] }) }, client.id)))) : (_jsx("div", { children: _jsx("span", { className: "text-sm text-muted-foreground", children: t('awesomeMenu.noActiveAdministrators') }) }))] }) })] }));
24
24
  };
@@ -16,5 +16,5 @@ export function ChannelSwitcher({ className }) {
16
16
  if (!channels || channels.length === 0) {
17
17
  return null;
18
18
  }
19
- return (_jsx("div", { className: className, children: _jsxs(Select, { defaultValue: selectedChannel?.id, onValueChange: onChannelChange, value: selectedChannel?.id, children: [_jsx(SelectTrigger, { className: cn('flex items-center gap-2 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate'), "aria-label": "Select an channel", children: _jsx(SelectValue, { children: _jsx("span", { className: "ml-2", children: getChannelLabel(channels.find((account) => account.id === selectedChannel?.id)?.code) }) }) }), _jsx(SelectContent, { children: channels.map((channel) => (_jsx(SelectItem, { value: channel.id, children: _jsx("div", { className: "flex items-center gap-3 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-foreground", children: getChannelLabel(channel.code) }) }, channel.code))) })] }) }));
19
+ return (_jsx("div", { className: className, children: _jsxs(Select, { defaultValue: selectedChannel?.id, onValueChange: onChannelChange, value: selectedChannel?.id, children: [_jsx(SelectTrigger, { className: cn('flex h-9 items-center gap-2 border-border/80 bg-card px-3 text-xs font-medium [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate'), "aria-label": "Select a channel", children: _jsx(SelectValue, { children: _jsx("span", { className: "ml-1", children: getChannelLabel(channels.find((account) => account.id === selectedChannel?.id)?.code) }) }) }), _jsx(SelectContent, { children: channels.map((channel) => (_jsx(SelectItem, { value: channel.id, children: _jsx("div", { className: "flex items-center gap-3 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-foreground", children: getChannelLabel(channel.code) }) }, channel.code))) })] }) }));
20
20
  }
@@ -23,5 +23,5 @@ export const LanguagesDropdown = () => {
23
23
  })));
24
24
  const Flag = langFlagDict[contentLng] || EU;
25
25
  const contentLanguages = selectedChannel?.availableLanguageCodes || [];
26
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: "h-9", children: [t('language'), _jsx(Badge, { variant: "outline", className: "ml-2", children: _jsx(Flag, { className: "size-4" }) })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "z-[150]", children: [_jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { children: _jsx("span", { children: t('content') }) }), _jsx(DropdownMenuPortal, { children: _jsx(DropdownMenuSubContent, { className: "z-[150]", children: contentLanguages.map((lng) => (_jsxs(DropdownMenuItem, { onClick: () => lng !== contentLng && setContentLng(lng), children: [_jsx("span", { children: getLanguageName(lng, uiLng) }), lng === contentLng && _jsx(Check, { className: "ml-auto size-4" })] }, lng))) }) })] }), _jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { children: _jsx("span", { children: "UI" }) }), _jsx(DropdownMenuPortal, { children: _jsx(DropdownMenuSubContent, { className: "z-[150]", children: uiLanguages.map((lng) => (_jsxs(DropdownMenuItem, { onClick: () => lng !== uiLng && setUiLng(lng), children: [_jsx("span", { children: getLanguageName(lng) }), lng === uiLng && _jsx(Check, { className: "ml-auto size-4" })] }, lng))) }) })] })] })] }));
26
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: "h-9 px-3 text-xs", children: [t('language'), _jsx(Badge, { variant: "outline", className: "ml-2 h-5 px-1.5", children: _jsx(Flag, { className: "size-4" }) })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "z-[150]", children: [_jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { children: _jsx("span", { children: t('content') }) }), _jsx(DropdownMenuPortal, { children: _jsx(DropdownMenuSubContent, { className: "z-[150]", children: contentLanguages.map((lng) => (_jsxs(DropdownMenuItem, { onClick: () => lng !== contentLng && setContentLng(lng), children: [_jsx("span", { children: getLanguageName(lng, uiLng) }), lng === contentLng && _jsx(Check, { className: "ml-auto size-4" })] }, lng))) }) })] }), _jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { children: _jsx("span", { children: "UI" }) }), _jsx(DropdownMenuPortal, { children: _jsx(DropdownMenuSubContent, { className: "z-[150]", children: uiLanguages.map((lng) => (_jsxs(DropdownMenuItem, { onClick: () => lng !== uiLng && setUiLng(lng), children: [_jsx("span", { children: getLanguageName(lng) }), lng === uiLng && _jsx(Check, { className: "ml-auto size-4" })] }, lng))) }) })] })] })] }));
27
27
  };
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { NavLink, useLocation } from 'react-router';
3
- import React, { useMemo } from 'react';
4
- import { cn, buttonVariants, Routes, usePluginStore, Tooltip, TooltipContent, TooltipTrigger, useServer, Accordion, AccordionItem, AccordionTrigger, AccordionContent, useNotifications, useTranslation, capitalizeFirstLetter, Skeleton, } from '@deenruv/react-ui-devkit';
5
- import { BarChart, Barcode, Store, ShoppingCart, Images, Folder, Globe2, Tag, UserCog, Users, UserRoundSearch, MapPin, Flag, Coins, Globe, Percent, CreditCard, Truck, Cog, UsersRound, Server, ScanBarcode, } from 'lucide-react';
6
- import { Permission } from '@deenruv/admin-types';
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { cn, buttonVariants, usePluginStore, Tooltip, TooltipContent, TooltipTrigger, useServer, Accordion, AccordionItem, AccordionTrigger, AccordionContent, useNotifications, useTranslation, capitalizeFirstLetter, Skeleton, } from '@deenruv/react-ui-devkit';
5
+ import { adminNavigationGroups, canAccessAdminItem, getActiveNavigationLinkIds, getActiveNavigationGroupIds, getNavigationLinkActivePaths, insertNavigationLink, useAdminAccess, } from "../../access/index.js";
7
6
  export function Navigation({ isCollapsed }) {
8
7
  const { t } = useTranslation('common');
9
8
  const { t: _pluginT } = useTranslation();
10
9
  const location = useLocation();
11
10
  const { navMenuData, viewMarkers } = usePluginStore();
11
+ const { routes } = useAdminAccess();
12
12
  const userPermissions = useServer((p) => p.userPermissions);
13
13
  const loaded = useServer((p) => p.loaded);
14
14
  const getNavigationNotification = useNotifications(({ getNavigationNotification }) => getNavigationNotification);
@@ -19,201 +19,34 @@ export function Navigation({ isCollapsed }) {
19
19
  return _pluginT(key, { ns });
20
20
  };
21
21
  const navigationGroups = useMemo(() => {
22
- const navData = [
23
- {
24
- label: t('menuGroups.shop'),
25
- id: 'shop-group',
26
- links: [
27
- { title: t('menu.dashboard'), href: Routes.dashboard, id: 'link-dashboard', icon: BarChart },
28
- {
29
- title: t('menu.orders'),
30
- href: Routes.orders.list,
31
- id: 'link-orders',
32
- icon: ShoppingCart,
33
- requiredPermissions: [Permission.ReadOrder],
34
- },
35
- {
36
- title: t('menu.customers'),
37
- href: Routes.customers.list,
38
- id: 'link-customers',
39
- icon: UserRoundSearch,
40
- requiredPermissions: [Permission.ReadCustomer],
41
- },
42
- {
43
- title: t('menu.customerGroups'),
44
- href: Routes.customerGroups.list,
45
- id: 'link-customerGroups',
46
- icon: UsersRound,
47
- requiredPermissions: [Permission.ReadCustomerGroup],
48
- },
49
- ],
50
- },
51
- {
52
- label: t('menuGroups.assortment'),
53
- id: 'assortment-group',
54
- links: [
55
- {
56
- title: t('menu.products'),
57
- href: Routes.products.list,
58
- id: 'link-products',
59
- icon: Barcode,
60
- requiredPermissions: [Permission.ReadProduct, Permission.ReadCatalog],
61
- },
62
- {
63
- title: t('menu.productVariants'),
64
- href: Routes.productVariants.list,
65
- id: 'link-product-variants',
66
- icon: ScanBarcode,
67
- requiredPermissions: [Permission.ReadProduct, Permission.ReadCatalog],
68
- },
69
- {
70
- title: t('menu.collections'),
71
- href: Routes.collections.list,
72
- id: 'link-collections',
73
- icon: Folder,
74
- requiredPermissions: [Permission.ReadCollection, Permission.ReadCatalog],
75
- },
76
- {
77
- title: t('menu.facets'),
78
- href: Routes.facets.list,
79
- id: 'link-facets',
80
- icon: Tag,
81
- requiredPermissions: [Permission.ReadFacet, Permission.ReadCatalog],
82
- },
83
- {
84
- title: t('menu.assets'),
85
- href: Routes.assets.list,
86
- id: 'link-assets',
87
- icon: Images,
88
- requiredPermissions: [Permission.ReadAsset, Permission.ReadCatalog],
89
- },
90
- ],
91
- },
92
- {
93
- label: t('menuGroups.users'),
94
- id: 'users-group',
95
- links: [
96
- {
97
- title: t('menu.admins'),
98
- href: Routes.admins.list,
99
- id: 'link-admins',
100
- icon: UserCog,
101
- requiredPermissions: [Permission.ReadAdministrator],
102
- },
103
- {
104
- title: t('menu.roles'),
105
- href: Routes.roles.list,
106
- id: 'link-roles',
107
- icon: Users,
108
- requiredPermissions: [Permission.ReadAdministrator],
109
- },
110
- {
111
- title: t('menu.sellers'),
112
- href: Routes.sellers.list,
113
- id: 'link-sellers',
114
- icon: Store,
115
- requiredPermissions: [Permission.ReadSeller],
116
- },
117
- ],
118
- },
119
- {
120
- label: t('menuGroups.promotions'),
121
- id: 'promotions-group',
122
- links: [
123
- {
124
- title: t('menu.promotions'),
125
- href: Routes.promotions.list,
126
- id: 'link-promotions',
127
- icon: ShoppingCart,
128
- requiredPermissions: [Permission.ReadPromotion],
129
- },
130
- ],
131
- },
132
- {
133
- label: t('menuGroups.shipping'),
134
- id: 'shipping-group',
135
- links: [
136
- {
137
- title: t('menu.paymentMethods'),
138
- href: Routes.paymentMethods.list,
139
- id: 'link-payment-methods',
140
- icon: CreditCard,
141
- requiredPermissions: [Permission.ReadPaymentMethod],
142
- },
143
- {
144
- title: t('menu.shippingMethods'),
145
- href: Routes.shippingMethods.list,
146
- id: 'link-shipping-methods',
147
- icon: Truck,
148
- requiredPermissions: [Permission.ReadShippingMethod],
149
- },
150
- {
151
- title: t('menu.stock'),
152
- href: Routes.stockLocations.list,
153
- id: 'link-stock',
154
- icon: MapPin,
155
- requiredPermissions: [Permission.ReadStockLocation],
156
- },
157
- ],
158
- },
159
- {
160
- label: t('menuGroups.settings'),
161
- id: 'settings-group',
162
- links: [
163
- {
164
- title: t('menu.channels'),
165
- href: Routes.channels.list,
166
- id: 'link-channels',
167
- icon: Globe2,
168
- requiredPermissions: [Permission.ReadChannel],
169
- },
170
- {
171
- title: t('menu.zones'),
172
- href: Routes.zones.list,
173
- id: 'link-zones',
174
- icon: Globe,
175
- requiredPermissions: [Permission.ReadZone],
176
- },
177
- {
178
- title: t('menu.countries'),
179
- href: Routes.countries.list,
180
- id: 'link-countries',
181
- icon: Flag,
182
- requiredPermissions: [Permission.ReadCountry],
183
- },
184
- {
185
- title: t('menu.taxCategories'),
186
- href: Routes.taxCategories.list,
187
- id: 'link-tax-categories',
188
- icon: Coins,
189
- requiredPermissions: [Permission.ReadTaxCategory],
190
- },
191
- {
192
- title: t('menu.taxRates'),
193
- href: Routes.taxRates.list,
194
- id: 'link-tax-rates',
195
- icon: Percent,
196
- requiredPermissions: [Permission.ReadTaxRate],
197
- },
198
- {
199
- title: t('menu.globalSettings'),
200
- href: Routes.globalSettings,
201
- id: 'link-global-settings',
202
- icon: Cog,
203
- requiredPermissions: [Permission.ReadSettings],
204
- },
205
- {
206
- title: t('menu.systemStatus'),
207
- href: Routes.status,
208
- id: 'link-system-status',
209
- icon: Server,
210
- requiredPermissions: [Permission.ReadSystem],
211
- },
212
- ],
213
- },
214
- ];
22
+ const navData = adminNavigationGroups.map((group) => ({
23
+ label: t(`menuGroups.${group.labelKey}`),
24
+ id: group.id,
25
+ links: [],
26
+ }));
27
+ routes.forEach((route) => {
28
+ if (!route.nav)
29
+ return;
30
+ if (!canAccessAdminItem({ item: route, userPermissions }))
31
+ return;
32
+ const foundGroupIdx = navData.findIndex((group) => group.id === route.nav?.groupId);
33
+ if (foundGroupIdx === -1)
34
+ return;
35
+ navData[foundGroupIdx].links.push({
36
+ title: t(`menu.${route.nav.menuKey}`),
37
+ href: route.path,
38
+ id: route.nav.linkId,
39
+ icon: route.nav.icon,
40
+ access: route,
41
+ routeId: route.id,
42
+ groupId: route.nav.groupId,
43
+ activePaths: getNavigationLinkActivePaths(route, routes),
44
+ });
45
+ });
215
46
  const { groups, links } = navMenuData;
216
- groups.forEach(({ id, labelId, placement }) => {
47
+ groups.forEach(({ id, labelId, placement, access }) => {
48
+ if (!canAccessAdminItem({ item: access, userPermissions }))
49
+ return;
217
50
  let foundGroupIdx = -1;
218
51
  const newGroup = { id, label: pluginT(labelId), links: [] };
219
52
  if (placement?.groupId) {
@@ -226,46 +59,52 @@ export function Navigation({ isCollapsed }) {
226
59
  navData.splice(foundGroupIdx + 1, 0, newGroup);
227
60
  }
228
61
  });
229
- links.forEach(({ groupId, href, labelId, id, icon, placement }) => {
62
+ links.forEach(({ groupId, href, labelId, id, icon, placement, access }) => {
63
+ if (!canAccessAdminItem({ item: access, userPermissions }))
64
+ return;
230
65
  const foundGroupIdx = navData.findIndex((group) => group.id === groupId);
231
66
  if (foundGroupIdx == -1)
232
- throw new Error(`Navbar menu group with id ${groupId} was not found.\nPlugin navigation href: ${href}`);
233
- const newElement = { title: pluginT(labelId), label: pluginT(labelId), href: `/${href}`, id, icon };
67
+ return;
68
+ const newElement = {
69
+ title: pluginT(labelId),
70
+ label: pluginT(labelId),
71
+ href: `/${href}`,
72
+ id,
73
+ icon,
74
+ access,
75
+ groupId,
76
+ };
234
77
  if (!placement) {
235
78
  navData[foundGroupIdx].links.push(newElement);
236
79
  return;
237
80
  }
238
- const foundIndex = navData[foundGroupIdx].links.findIndex((item) => item.id === placement.linkId);
239
- const offset = placement.where === 'above' ? 0 : 1;
240
- navData[foundGroupIdx].links.splice(foundIndex + offset, 0, newElement);
81
+ navData[foundGroupIdx].links = insertNavigationLink(navData[foundGroupIdx].links, newElement, placement);
82
+ });
83
+ return navData.filter((group) => group.links.length > 0);
84
+ }, [navMenuData.groups, navMenuData.links, pluginT, routes, t, userPermissions]);
85
+ const permittedNavigationGroups = navigationGroups;
86
+ const activeGroupIds = useMemo(() => getActiveNavigationGroupIds(permittedNavigationGroups.flatMap((group) => group.links), location.pathname), [location.pathname, permittedNavigationGroups]);
87
+ const activeLinkIds = useMemo(() => new Set(getActiveNavigationLinkIds(permittedNavigationGroups.flatMap((group) => group.links), location.pathname).map((link) => link.id)), [location.pathname, permittedNavigationGroups]);
88
+ const previousPathname = useRef(location.pathname);
89
+ const [openGroupIds, setOpenGroupIds] = useState(activeGroupIds);
90
+ useEffect(() => {
91
+ const hasPathnameChanged = previousPathname.current !== location.pathname;
92
+ previousPathname.current = location.pathname;
93
+ if (!hasPathnameChanged)
94
+ return;
95
+ setOpenGroupIds((currentGroupIds) => {
96
+ const nextGroupIds = [...new Set([...currentGroupIds, ...activeGroupIds])];
97
+ return nextGroupIds.length === currentGroupIds.length ? currentGroupIds : nextGroupIds;
241
98
  });
242
- return navData;
243
- }, [navMenuData.groups, navMenuData.links, t]);
244
- const permittedNavigationGroups = useMemo(() => {
245
- return navigationGroups
246
- .map((group) => ({
247
- ...group,
248
- links: group.links.filter((link) => 'requiredPermissions' in link
249
- ? link.requiredPermissions?.some((permission) => userPermissions.includes(permission))
250
- : true),
251
- }))
252
- .filter((group) => group.links.length > 0);
253
- }, [userPermissions, navigationGroups]);
254
- // const defaultAccordionOpenValue = useMemo(
255
- // () =>
256
- // permittedNavigationGroups
257
- // .filter((g) => !navMenuData.groups.find((pluginGroup) => pluginGroup.id === g.id))
258
- // .map((g) => g.id),
259
- // [permittedNavigationGroups, navMenuData],
260
- // );
261
- const defaultAccordionOpenValue = ['shop-group', 'assortment-group'];
99
+ }, [activeGroupIds, location.pathname]);
262
100
  if (!loaded) {
263
- return (_jsx("div", { className: "relative overflow-y-auto", children: _jsx("div", { className: "flex h-[calc(100%-70px)] flex-col gap-4 pb-2 lg:h-[calc(100%-80px)]", children: Array.from({ length: 3 }).map((_, groupIdx) => (_jsxs("div", { className: "flex flex-col gap-1 px-2", children: [!isCollapsed && (_jsx("div", { className: "px-4 py-2", children: _jsx(Skeleton, { className: "h-3 w-20" }) })), Array.from({ length: groupIdx === 0 ? 4 : groupIdx === 1 ? 5 : 3 }).map((_, linkIdx) => (_jsxs("div", { className: cn('flex items-center rounded-md px-4 py-2', isCollapsed && 'justify-center px-0'), children: [_jsx(Skeleton, { className: cn('size-4 shrink-0', isCollapsed && 'size-6') }), !isCollapsed && _jsx(Skeleton, { className: "ml-2 h-4 w-24" })] }, linkIdx)))] }, groupIdx))) }) }));
101
+ return (_jsx("div", { className: "relative overflow-y-auto", children: _jsx("div", { className: "flex h-[calc(100%-64px)] flex-col gap-3 pb-2 lg:h-[calc(100%-72px)]", children: Array.from({ length: 3 }).map((_, groupIdx) => (_jsxs("div", { className: "flex flex-col gap-1 px-2.5", children: [!isCollapsed && (_jsx("div", { className: "px-3 py-2", children: _jsx(Skeleton, { className: "h-3 w-20" }) })), Array.from({ length: groupIdx === 0 ? 4 : groupIdx === 1 ? 5 : 3 }).map((_, linkIdx) => (_jsxs("div", { className: cn('flex items-center px-3 py-2', isCollapsed && 'justify-center px-0'), children: [_jsx(Skeleton, { className: cn('size-4 shrink-0', isCollapsed && 'size-6') }), !isCollapsed && _jsx(Skeleton, { className: "ml-2 h-4 w-24" })] }, linkIdx)))] }, groupIdx))) }) }));
264
102
  }
265
- return (_jsx("div", { className: "relative overflow-y-auto", children: _jsx("div", { "data-collapsed": isCollapsed, className: "group flex h-[calc(100%-70px)] flex-col gap-4 pb-2 data-[collapsed=true]:py-2 lg:h-[calc(100%-80px)]", children: _jsx(Accordion, { type: "multiple", className: "w-full", defaultValue: defaultAccordionOpenValue, value: isCollapsed ? permittedNavigationGroups.map((g) => g.id) : undefined, children: permittedNavigationGroups.map((group) => (_jsxs(AccordionItem, { value: group.id, children: [!isCollapsed && (_jsx(AccordionTrigger, { className: cn('flex items-center justify-between pr-3 hover:no-underline'), children: _jsxs("div", { className: "flex items-center gap-2 px-6", children: [_jsx("h4", { className: "text-xs font-bold uppercase hover:underline", children: group.label }), getNavigationNotification(group.id), viewMarkers ? (_jsx("p", { className: "text-xs font-semibold text-muted-foreground lowercase dark:text-muted-foreground", children: group.id })) : null] }) })), _jsx(AccordionContent, { className: cn(isCollapsed ? 'py-2' : 'pb-4'), children: _jsx("nav", { id: group.id, className: "grid gap-1 px-2 text-muted-foreground group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2", children: group.links.map((link, index) => {
103
+ return (_jsx("div", { className: "relative overflow-y-auto", children: _jsx("div", { "data-collapsed": isCollapsed, className: "group flex h-[calc(100%-64px)] flex-col gap-2 py-2 data-[collapsed=true]:py-2 lg:h-[calc(100%-72px)]", children: _jsx(Accordion, { type: "multiple", className: "w-full", value: isCollapsed ? permittedNavigationGroups.map((g) => g.id) : openGroupIds, onValueChange: setOpenGroupIds, children: permittedNavigationGroups.map((group) => (_jsxs(AccordionItem, { value: group.id, className: "border-none", children: [!isCollapsed && (_jsx(AccordionTrigger, { className: cn('flex items-center justify-between px-3 py-2 hover:no-underline'), children: _jsxs("div", { className: "flex items-center gap-2 px-1", children: [_jsx("h4", { className: "text-[11px] font-semibold tracking-[0.08em] text-muted-foreground uppercase", children: group.label }), getNavigationNotification(group.id), viewMarkers ? (_jsx("p", { className: "text-xs font-semibold text-muted-foreground lowercase dark:text-muted-foreground", children: group.id })) : null] }) })), _jsx(AccordionContent, { className: cn(isCollapsed ? 'py-1' : 'pb-2'), children: _jsx("nav", { id: group.id, className: "grid gap-1 px-2.5 text-muted-foreground group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2", children: group.links.map((link, index) => {
266
104
  const notifications = getNavigationNotification(link.id);
267
- return (_jsx(React.Fragment, { children: isCollapsed ? (_jsxs(Tooltip, { delayDuration: 0, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("div", { children: _jsx(NavLink, { to: link.href, viewTransition: true, children: _jsxs("div", { className: cn(buttonVariants({ variant: 'navigation-link', size: 'icon' }), 'h-9 w-9', location.pathname === link.href &&
268
- 'bg-muted opacity-100 hover:bg-muted hover:text-muted-foreground dark:bg-muted dark:hover:bg-muted'), children: [_jsx(link.icon, { className: "size-6" }), _jsx("span", { className: "sr-only", children: link.title })] }) }) }) }), _jsxs(TooltipContent, { side: "right", className: "relative flex items-center gap-4", children: [viewMarkers ? (_jsx("div", { className: "text-xs font-semibold text-muted-foreground lowercase dark:text-muted-foreground", children: link.id })) : null, capitalizeFirstLetter(link.title), notifications] })] }, index)) : (_jsx(NavLink, { to: link.href, viewTransition: true, children: _jsxs("div", { id: link.id, className: cn('relative flex items-center justify-start rounded-md px-4 py-2 capitalize', location.pathname === link.href &&
269
- 'bg-muted font-semibold opacity-100 hover:bg-muted hover:text-muted-foreground dark:bg-muted dark:hover:bg-muted'), children: [viewMarkers ? (_jsx("div", { className: "absolute top-1/2 right-2 -translate-y-1/2 text-xs font-semibold text-muted-foreground lowercase dark:text-muted-foreground", children: link.id })) : null, _jsx(link.icon, { className: "mr-2 size-4" }), capitalizeFirstLetter(link.title), notifications] }) })) }, link.id));
105
+ const isActive = activeLinkIds.has(link.id);
106
+ return (_jsx(React.Fragment, { children: isCollapsed ? (_jsxs(Tooltip, { delayDuration: 0, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("div", { children: _jsx(NavLink, { to: link.href, viewTransition: true, "aria-current": isActive ? 'page' : undefined, children: _jsxs("div", { className: cn(buttonVariants({ variant: 'navigation-link', size: 'icon' }), 'h-9 w-9 rounded-lg ring-offset-background transition-all hover:scale-[1.03] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', isActive &&
107
+ 'bg-primary text-primary-foreground opacity-100 shadow-sm hover:scale-100 hover:bg-primary hover:text-primary-foreground'), children: [_jsx(link.icon, { className: "size-5" }), _jsx("span", { className: "sr-only", children: link.title })] }) }) }) }), _jsxs(TooltipContent, { side: "right", className: "relative flex items-center gap-4", children: [viewMarkers ? (_jsx("div", { className: "text-xs font-semibold text-muted-foreground lowercase dark:text-muted-foreground", children: link.id })) : null, capitalizeFirstLetter(link.title), notifications] })] }, index)) : (_jsx(NavLink, { to: link.href, viewTransition: true, "aria-current": isActive ? 'page' : undefined, children: _jsxs("div", { id: link.id, className: cn('relative flex h-10 items-center justify-start rounded-lg px-3 text-sm font-medium capitalize transition-all hover:bg-muted hover:text-foreground', isActive &&
108
+ 'bg-primary text-primary-foreground opacity-100 shadow-sm hover:bg-primary hover:text-primary-foreground'), children: [viewMarkers ? (_jsx("div", { className: "absolute top-1/2 right-2 -translate-y-1/2 text-xs font-semibold text-muted-foreground lowercase dark:text-muted-foreground", children: link.id })) : null, _jsx("span", { className: cn('mr-2.5 flex size-6 shrink-0 items-center justify-center rounded-md bg-muted/80 text-muted-foreground transition-colors', isActive && 'bg-primary-foreground/15 text-primary-foreground'), children: _jsx(link.icon, { className: "size-4" }) }), _jsx("span", { className: "truncate", children: capitalizeFirstLetter(link.title) }), notifications] }) })) }, link.id));
270
109
  }) }) })] }, group.id))) }) }) }));
271
110
  }
@@ -1,12 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Permission } from '@deenruv/admin-types';
3
2
  import { capitalizeFirstLetter, cn, Routes, Separator, useServer, useTranslation } from '@deenruv/react-ui-devkit';
4
3
  import { Puzzle } from 'lucide-react';
5
4
  import { NavLink } from 'react-router';
5
+ import { canAccessAdminItem, useAdminAccess } from "../../access/index.js";
6
6
  export const NavigationFooter = ({ isCollapsed }) => {
7
7
  const { t } = useTranslation('common');
8
8
  const userPermissions = useServer((p) => p.userPermissions);
9
- const isPermittedToExtensions = userPermissions.includes(Permission.ReadSettings);
10
- return (_jsxs("div", { className: "flex w-full flex-col gap-2 bg-secondary py-2 text-xs shadow-2xl select-none", children: [isPermittedToExtensions && !isCollapsed && (_jsxs(_Fragment, { children: [_jsx("div", { children: _jsx(NavLink, { to: Routes.extensions, viewTransition: true, children: _jsxs("div", { className: cn('relative flex items-center justify-center rounded-md px-4 capitalize', location.pathname === Routes.extensions &&
11
- 'bg-muted font-semibold opacity-100 hover:bg-muted hover:text-muted-foreground dark:bg-muted dark:hover:bg-muted'), children: [_jsx(Puzzle, { className: "mr-2 size-4" }), capitalizeFirstLetter(t('menu.extensions'))] }) }) }), _jsx(Separator, {})] })), _jsxs("div", { className: "flex items-center justify-center gap-1", children: [!isCollapsed && _jsx("p", { className: "uppercase", children: "Deenruv" }), _jsxs("span", { children: [!isCollapsed ? 'ver. ' : 'v. ', window.__DEENRUV_SETTINGS__.appVersion] })] })] }));
9
+ const { routes } = useAdminAccess();
10
+ const extensionsRoute = routes.find((route) => route.id === 'extensions');
11
+ const isPermittedToExtensions = !!extensionsRoute && canAccessAdminItem({ item: extensionsRoute, userPermissions });
12
+ const extensionsPath = extensionsRoute?.path || Routes.extensions;
13
+ return (_jsxs("div", { className: "flex w-full flex-col gap-2 border-t bg-card/80 py-2 text-xs text-muted-foreground select-none", children: [isPermittedToExtensions && !isCollapsed && (_jsxs(_Fragment, { children: [_jsx("div", { children: _jsx(NavLink, { to: extensionsPath, viewTransition: true, children: _jsxs("div", { className: cn('relative mx-2 flex h-8 items-center justify-center px-3 text-sm font-medium capitalize transition-colors hover:bg-muted/70 hover:text-foreground', location.pathname === extensionsPath &&
14
+ 'bg-primary/10 text-primary opacity-100 hover:bg-primary/10 hover:text-primary'), children: [_jsx(Puzzle, { className: "mr-2 size-4" }), capitalizeFirstLetter(t('menu.extensions'))] }) }) }), _jsx(Separator, {})] })), _jsxs("div", { className: "flex items-center justify-center gap-1 px-2", children: [!isCollapsed && _jsx("p", { className: "uppercase", children: "Deenruv" }), _jsxs("span", { children: [!isCollapsed ? 'ver. ' : 'v. ', window.__DEENRUV_SETTINGS__.appVersion] })] })] }));
12
15
  };
@@ -87,5 +87,5 @@ export const Notifications = () => {
87
87
  prevJobQueuesRef.current = [...jobQueues];
88
88
  }
89
89
  }, [jobQueues, status]);
90
- return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "icon", className: "relative size-10", children: [_jsx(Bell, { className: "size-4" }), hasUnread && (_jsx("span", { className: "absolute top-1.5 right-1.5 size-2 rounded-full bg-red-500", children: _jsx("span", { className: "absolute inset-0 animate-ping rounded-full bg-red-400 opacity-75" }) })), _jsx("span", { className: "sr-only", children: t('notificationsBox.toggleNotifications') })] }) }), _jsx(PopoverContent, { className: "z-[2138] w-md p-0", align: "end", children: _jsx(Tabs, { defaultValue: "ALL", className: "w-full", children: _jsxs(Card, { className: "w-full border-0 shadow-none", children: [_jsxs(CardHeader, { className: cn('flex w-full flex-col items-start justify-start gap-2 bg-muted/30 p-0', components.length === 0 && 'pb-3'), children: [_jsxs("div", { className: "flex h-full w-full flex-1 items-center justify-between px-4 pt-3", children: [_jsxs("div", { className: "flex flex-col", children: [_jsx(CardTitle, { className: "text-base", children: t('notificationsBox.notifications') }), _jsx(CardDescription, { className: "mt-1 text-xs", children: t('notificationsBox.recentNotifications') })] }), notifications.length > 0 && (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 text-xs", onClick: clearAllNotifications, children: [_jsx(Trash2, { className: "mr-1 size-3" }), t('notificationsBox.clearAll')] }))] }), components.length !== 0 && (_jsxs(TabsList, { className: "m-0 w-full rounded-none", children: [_jsx(TabsTrigger, { value: "ALL", className: "w-full rounded-none text-left", children: t('notificationsBox.all') }), cyclingNotifications.map((notification) => (_jsx(TabsTrigger, { value: notification.id, className: "w-full rounded-none text-left", children: notification.id }, notification.id)))] }))] }), _jsxs(CardContent, { className: "max-h-[400px] overflow-auto p-0", children: [_jsx(TabsContent, { value: "ALL", className: "mt-0", children: notifications.length > 0 ? (_jsx("div", { className: "divide-y", children: notifications.map((notification) => (_jsxs("div", { className: `flex items-start gap-3 p-4 transition-colors ${notification.read ? 'bg-background' : 'bg-muted/20'}`, children: [_jsx("div", { className: "mt-0.5", children: notification.icon }), _jsxs("div", { className: "flex-1 space-y-1", children: [_jsx("p", { className: "text-sm leading-none font-medium", children: notification.title }), _jsx("p", { className: "text-xs text-muted-foreground", children: notification.description }), _jsxs("div", { className: "flex items-center pt-1", children: [_jsx(Clock, { className: "mr-1 size-3 text-muted-foreground" }), _jsx("span", { className: "text-xs text-muted-foreground", children: notification.time })] })] })] }, notification.id))) })) : (_jsxs("div", { className: "flex flex-col items-center justify-center px-4 py-8 text-center", children: [_jsx("div", { className: "mb-3 rounded-full bg-muted p-3", children: _jsx(Bell, { className: "size-6 text-muted-foreground" }) }), _jsx("h3", { className: "text-sm font-medium", children: t('notificationsBox.emptyStateMessage') }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t('notificationsBox.emptyStateDescription') })] })) }), components.map((notification) => (_jsx(TabsContent, { value: notification.id, children: notification.component }, notification.id)))] }), hasUnread && (_jsx(CardFooter, { className: "flex items-center justify-end p-4", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 text-xs", onClick: markAllAsRead, children: [_jsx(Check, { className: "mr-1 size-3" }), t('notificationsBox.markAllAsRead')] }) }))] }) }) })] }));
90
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "icon", className: "relative size-9", children: [_jsx(Bell, { className: "size-4" }), hasUnread && (_jsx("span", { className: "absolute top-1.5 right-1.5 size-2 rounded-full bg-red-500", children: _jsx("span", { className: "absolute inset-0 animate-ping rounded-full bg-red-400 opacity-75" }) })), _jsx("span", { className: "sr-only", children: t('notificationsBox.toggleNotifications') })] }) }), _jsx(PopoverContent, { className: "z-[2138] w-[min(28rem,calc(100vw-2rem))] p-0", align: "end", children: _jsx(Tabs, { defaultValue: "ALL", className: "w-full", children: _jsxs(Card, { className: "w-full border-0 shadow-none", children: [_jsxs(CardHeader, { className: cn('flex w-full flex-col items-start justify-start gap-2 bg-card p-0', components.length === 0 && 'pb-3'), children: [_jsxs("div", { className: "flex h-full w-full flex-1 items-center justify-between px-4 pt-3", children: [_jsxs("div", { className: "flex flex-col", children: [_jsx(CardTitle, { className: "text-base", children: t('notificationsBox.notifications') }), _jsx(CardDescription, { className: "mt-1 text-xs", children: t('notificationsBox.recentNotifications') })] }), notifications.length > 0 && (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 text-xs", onClick: clearAllNotifications, children: [_jsx(Trash2, { className: "mr-1 size-3" }), t('notificationsBox.clearAll')] }))] }), components.length !== 0 && (_jsxs(TabsList, { className: "m-0 w-full bg-muted/50", children: [_jsx(TabsTrigger, { value: "ALL", className: "w-full text-left", children: t('notificationsBox.all') }), cyclingNotifications.map((notification) => (_jsx(TabsTrigger, { value: notification.id, className: "w-full text-left", children: notification.id }, notification.id)))] }))] }), _jsxs(CardContent, { className: "max-h-[400px] overflow-auto p-0", children: [_jsx(TabsContent, { value: "ALL", className: "mt-0", children: notifications.length > 0 ? (_jsx("div", { className: "divide-y", children: notifications.map((notification) => (_jsxs("div", { className: `flex items-start gap-3 p-4 transition-colors ${notification.read ? 'bg-card' : 'bg-accent/40'}`, children: [_jsx("div", { className: "mt-0.5", children: notification.icon }), _jsxs("div", { className: "flex-1 space-y-1", children: [_jsx("p", { className: "text-sm leading-none font-medium", children: notification.title }), _jsx("p", { className: "text-xs text-muted-foreground", children: notification.description }), _jsxs("div", { className: "flex items-center pt-1", children: [_jsx(Clock, { className: "mr-1 size-3 text-muted-foreground" }), _jsx("span", { className: "text-xs text-muted-foreground", children: notification.time })] })] })] }, notification.id))) })) : (_jsxs("div", { className: "flex flex-col items-center justify-center px-4 py-8 text-center", children: [_jsx("div", { className: "mb-3 bg-muted p-3", children: _jsx(Bell, { className: "size-6 text-muted-foreground" }) }), _jsx("h3", { className: "text-sm font-medium", children: t('notificationsBox.emptyStateMessage') }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t('notificationsBox.emptyStateDescription') })] })) }), components.map((notification) => (_jsx(TabsContent, { value: notification.id, children: notification.component }, notification.id)))] }), hasUnread && (_jsx(CardFooter, { className: "flex items-center justify-end p-4", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 text-xs", onClick: markAllAsRead, children: [_jsx(Check, { className: "mr-1 size-3" }), t('notificationsBox.markAllAsRead')] }) }))] }) }) })] }));
91
91
  };