@deenruv/admin-dashboard 1.0.17-dev.7 → 1.0.17-dev.9

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.
@@ -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
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%-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", defaultValue: defaultAccordionOpenValue, value: isCollapsed ? permittedNavigationGroups.map((g) => g.id) : undefined, 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) => {
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-primary/10 text-primary opacity-100 hover:bg-primary/10 hover:text-primary'), 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, children: _jsxs("div", { id: link.id, className: cn('relative flex h-9 items-center justify-start px-3 text-sm font-medium capitalize transition-colors hover:bg-muted/70 hover:text-foreground', location.pathname === link.href &&
269
- 'bg-primary/10 text-primary opacity-100 hover:bg-primary/10 hover:text-primary'), 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 shrink-0" }), 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 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: Routes.extensions, 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 === Routes.extensions &&
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 &&
11
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
  };
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useMemo, useState } from 'react';
3
3
  import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbSeparator, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, ScrollArea, TooltipProvider, Routes, useSettings, useServer, usePluginStore, cn, dashToCamelCase, apiClient, useGlobalSearch, useTranslation, DropdownMenuGroup, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuPortal, DropdownMenuSubContent, createDialog, buildURL, } from '@deenruv/react-ui-devkit';
4
+ import { Permission } from '@deenruv/admin-types';
4
5
  import { useShallow } from 'zustand/react/shallow';
5
6
  import { GripVertical, LogOutIcon, MenuIcon, Moon, Slash, Sun, SunMoon, RotateCwSquare, SearchIcon, } from 'lucide-react';
6
7
  import * as ResizablePrimitive from 'react-resizable-panels';
@@ -11,6 +12,7 @@ import { BrandLogo } from "../BrandLogo.js";
11
12
  import { LanguagesDropdown } from './LanguagesDropdown.js';
12
13
  import { Notifications } from './Notifications.js';
13
14
  import { NavigationFooter } from "./NavigationFooter.js";
15
+ import { canAccessAdminItem, useAdminAccess } from "../../access/index.js";
14
16
  const ResizablePanelGroup = ({ className, ...props }) => (_jsx(ResizablePrimitive.Group, { className: cn('flex h-full w-full data-[orientation=vertical]:flex-col', className), ...props }));
15
17
  const ResizablePanel = ResizablePrimitive.Panel;
16
18
  const ResizableHandle = ({ withHandle, className, ...props }) => (_jsx(ResizablePrimitive.Separator, { className: cn('relative flex w-px items-center justify-center bg-border/70 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:translate-x-0 data-[orientation=vertical]:after:-translate-y-1/2 [&[data-orientation=vertical]>div]:rotate-90', className), ...props, children: withHandle && (_jsx("div", { className: "z-10 flex h-4 w-3 items-center justify-center border bg-background text-muted-foreground", children: _jsx(GripVertical, { className: "size-2.5" }) })) }));
@@ -20,6 +22,7 @@ export const Menu = ({ children }) => {
20
22
  const linkPath = [];
21
23
  const { t } = useTranslation('common');
22
24
  const { topNavigationActionsMenu, topNavigationComponents } = usePluginStore();
25
+ const { routes, defaultRoute } = useAdminAccess();
23
26
  const { logOut, theme, setTheme } = useSettings(useShallow((p) => ({
24
27
  logOut: p.logOut,
25
28
  theme: p.theme,
@@ -29,7 +32,36 @@ export const Menu = ({ children }) => {
29
32
  })));
30
33
  const navigate = useNavigate();
31
34
  const [isCollapsed, setIsCollapsed] = useState(false);
32
- const { activeAdministrator, setJobQueue } = useServer();
35
+ const { activeAdministrator, clearAdministratorAccess, setJobQueue, userPermissions } = useServer();
36
+ const defaultRoutePath = defaultRoute?.path || Routes.dashboard;
37
+ const defaultRouteMenuKey = defaultRoute?.nav?.menuKey || defaultRoute?.search?.menuKey || 'dashboard';
38
+ const globalSearchEnabled = true;
39
+ const languageSwitcherEnabled = true;
40
+ const channelSwitcherEnabled = true;
41
+ const notificationsEnabled = true;
42
+ const reindexEnabled = canAccessAdminItem({
43
+ item: { requiredPermissions: [Permission.UpdateCatalog, Permission.UpdateProduct] },
44
+ userPermissions,
45
+ });
46
+ const canAccessPluginSurface = (entry) => canAccessAdminItem({ item: entry.access, userPermissions });
47
+ const allowedTopNavigationComponents = topNavigationComponents?.filter(canAccessPluginSurface);
48
+ const allowedTopNavigationActions = topNavigationActionsMenu?.filter(canAccessPluginSurface);
49
+ const systemStatusRoute = routes.find((route) => route.id === 'system.status');
50
+ const globalSettingsRoute = routes.find((route) => route.id === 'settings.global');
51
+ const fastLinks = [
52
+ systemStatusRoute &&
53
+ canAccessAdminItem({ item: systemStatusRoute, userPermissions }) && {
54
+ key: 'systemStatus',
55
+ label: t('systemStatus'),
56
+ path: systemStatusRoute.path,
57
+ },
58
+ globalSettingsRoute &&
59
+ canAccessAdminItem({ item: globalSettingsRoute, userPermissions }) && {
60
+ key: 'globalSettings',
61
+ label: t('globalSettings'),
62
+ path: globalSettingsRoute.path,
63
+ },
64
+ ].filter(Boolean);
33
65
  const rebuildSearchIndex = async () => {
34
66
  await apiClient('mutation')({ reindex: { id: true, queueName: true, state: true } }).then(({ reindex: { queueName, state } }) => {
35
67
  setJobQueue(queueName, state === 'RUNNING');
@@ -48,10 +80,12 @@ export const Menu = ({ children }) => {
48
80
  const collapsed = size.asPercentage <= 4;
49
81
  setIsCollapsed(collapsed);
50
82
  document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(collapsed)}`;
51
- }, className: cn(isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out'), children: [_jsx("div", { className: cn('flex h-[72px] flex-col items-center justify-center gap-4 border-b bg-card/80'), children: _jsx("div", { className: `flex h-full items-center justify-center ${!isCollapsed && 'w-full'} cursor-pointer px-4 py-3`, onClick: () => navigate(Routes.dashboard, { viewTransition: true }), children: _jsx(BrandLogo, { isCollapsed: isCollapsed }) }) }), _jsxs("div", { className: "flex h-[calc(100vh-72px)] flex-col justify-between bg-card/70", children: [_jsx(Navigation, { isCollapsed: isCollapsed }), _jsx(NavigationFooter, { isCollapsed: isCollapsed })] })] }), _jsx(ResizableHandle, { withHandle: true }), _jsxs(ResizablePanel, { id: "content", children: [_jsxs("div", { className: "flex h-[64px] items-center border-b bg-background/95 px-3 backdrop-blur lg:h-[72px] lg:px-5", children: [_jsxs("div", { className: "flex flex-col items-start justify-center", children: [_jsx(Breadcrumb, { children: _jsx(BreadcrumbList, { children: crumbs.length ? (crumbs.map((c, i) => {
83
+ }, className: cn(isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out'), children: [_jsx("div", { className: cn('flex h-[72px] flex-col items-center justify-center gap-4 border-b bg-card/80'), children: _jsx("div", { className: `flex h-full items-center justify-center ${!isCollapsed && 'w-full'} cursor-pointer px-4 py-3`, onClick: () => navigate(defaultRoutePath, { viewTransition: true }), children: _jsx(BrandLogo, { isCollapsed: isCollapsed }) }) }), _jsxs("div", { className: "flex h-[calc(100vh-72px)] flex-col justify-between bg-card/70", children: [_jsx(Navigation, { isCollapsed: isCollapsed }), _jsx(NavigationFooter, { isCollapsed: isCollapsed })] })] }), _jsx(ResizableHandle, { withHandle: true }), _jsxs(ResizablePanel, { id: "content", children: [_jsxs("div", { className: "flex h-[64px] items-center border-b bg-background/95 px-3 backdrop-blur lg:h-[72px] lg:px-5", children: [_jsxs("div", { className: "flex flex-col items-start justify-center", children: [_jsx(Breadcrumb, { children: _jsx(BreadcrumbList, { children: crumbs.length ? (crumbs.map((c, i) => {
52
84
  linkPath.push(c);
53
85
  return (_jsxs(React.Fragment, { children: [_jsx(BreadcrumbItem, { children: _jsx(NavLink, { to: buildURL(linkPath), viewTransition: true, children: _jsx("p", { className: cn('text-sm font-semibold tracking-tight text-foreground capitalize'), children: i === 0 ? t('menu.' + dashToCamelCase(c)) : c }) }) }), i !== crumbs.length - 1 && (_jsx(BreadcrumbSeparator, { children: _jsx(Slash, { className: "text-muted-foreground" }) }))] }, c));
54
- })) : (_jsx(BreadcrumbItem, { children: _jsx(NavLink, { to: Routes.dashboard, viewTransition: true, children: _jsx("p", { className: "text-xl font-semibold tracking-tight text-foreground", children: t('dashboard') }) }) })) }) }), _jsx("div", { className: "flex items-center gap-2" })] }), _jsxs("div", { className: "flex flex-1 items-center justify-end gap-1.5", children: [topNavigationComponents && topNavigationComponents.length > 0 ? (_jsx("div", { className: "flex items-center gap-2", children: topNavigationComponents.map(({ component: Component }, index) => (_jsx(Component, {}, index))) })) : null, _jsx(LanguagesDropdown, {}), _jsx(ChannelSwitcher, { className: "min-w-44" }), _jsx(Button, { onClick: openGlobalSearch, variant: "outline", size: "icon", className: "relative size-9", children: _jsx(SearchIcon, { className: "size-4" }) }), _jsx(Notifications, {}), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "icon", className: "size-9", children: [theme === 'light' ? (_jsx(Sun, { className: "size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" })) : theme === 'dark' ? (_jsx(Moon, { className: "absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" })) : (_jsx(SunMoon, { className: "absolute size-[1.2rem] rotate-90 transition-all" })), _jsx("span", { className: "sr-only", children: t('toggleTheme') })] }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => setTheme('light'), children: t('themeLight') }), _jsx(DropdownMenuItem, { onClick: () => setTheme('dark'), children: t('themeDark') }), _jsx(DropdownMenuItem, { onClick: () => setTheme('system'), children: t('themeSystem') })] })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon", className: "size-9", children: _jsx(MenuIcon, { className: "size-4" }) }) }), _jsxs(DropdownMenuContent, { className: "z-[150] mr-6 min-w-40", children: [activeAdministrator?.emailAddress && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuLabel, { className: "flex items-center gap-2 px-3 py-2 font-medium", children: [_jsx("div", { className: "flex size-6 items-center justify-center bg-primary/10 text-xs font-semibold text-primary", children: activeAdministrator.firstName.charAt(0).toUpperCase() }), _jsxs("div", { className: "truncate text-sm", children: [activeAdministrator.firstName, " ", activeAdministrator.lastName] })] }), _jsx(DropdownMenuSeparator, { className: "my-1" })] })), _jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2 text-nowrap", onSelect: rebuildSearchIndex, children: [_jsx(RotateCwSquare, { className: "size-4" }), t('rebuildSerachIndex')] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { children: t('fastLinks') }), _jsx(DropdownMenuPortal, { children: _jsxs(DropdownMenuSubContent, { children: [_jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2 text-nowrap", onSelect: () => navigate(Routes.status, { viewTransition: true }), children: [_jsx(RotateCwSquare, { className: "size-4" }), t('systemStatus')] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2 text-nowrap", onSelect: () => navigate(Routes.globalSettings, { viewTransition: true }), children: [_jsx(RotateCwSquare, { className: "size-4" }), t('globalSettings')] })] }) })] }) }), topNavigationActionsMenu?.length && topNavigationActionsMenu.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), topNavigationActionsMenu.map((action) => (_jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2", onSelect: action.onClick, children: [action.icon && _jsx(action.icon, { className: "size-4" }), action.label] }, action.label)))] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2 text-red-500", onSelect: async () => {
86
+ })) : (_jsx(BreadcrumbItem, { children: _jsx(NavLink, { to: defaultRoutePath, viewTransition: true, children: _jsx("p", { className: "text-xl font-semibold tracking-tight text-foreground", children: t(defaultRouteMenuKey === 'dashboard'
87
+ ? 'dashboard'
88
+ : `menu.${dashToCamelCase(defaultRouteMenuKey)}`) }) }) })) }) }), _jsx("div", { className: "flex items-center gap-2" })] }), _jsxs("div", { className: "flex flex-1 items-center justify-end gap-1.5", children: [allowedTopNavigationComponents && allowedTopNavigationComponents.length > 0 ? (_jsx("div", { className: "flex items-center gap-2", children: allowedTopNavigationComponents.map(({ component: Component }, index) => (_jsx(Component, {}, index))) })) : null, languageSwitcherEnabled && _jsx(LanguagesDropdown, {}), channelSwitcherEnabled && _jsx(ChannelSwitcher, { className: "min-w-44" }), globalSearchEnabled && (_jsx(Button, { onClick: openGlobalSearch, variant: "outline", size: "icon", className: "relative size-9", children: _jsx(SearchIcon, { className: "size-4" }) })), notificationsEnabled && _jsx(Notifications, {}), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "icon", className: "size-9", children: [theme === 'light' ? (_jsx(Sun, { className: "size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" })) : theme === 'dark' ? (_jsx(Moon, { className: "absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" })) : (_jsx(SunMoon, { className: "absolute size-[1.2rem] rotate-90 transition-all" })), _jsx("span", { className: "sr-only", children: t('toggleTheme') })] }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => setTheme('light'), children: t('themeLight') }), _jsx(DropdownMenuItem, { onClick: () => setTheme('dark'), children: t('themeDark') }), _jsx(DropdownMenuItem, { onClick: () => setTheme('system'), children: t('themeSystem') })] })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon", className: "size-9", children: _jsx(MenuIcon, { className: "size-4" }) }) }), _jsxs(DropdownMenuContent, { className: "z-[150] mr-6 min-w-40", children: [activeAdministrator?.emailAddress && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuLabel, { className: "flex items-center gap-2 px-3 py-2 font-medium", children: [_jsx("div", { className: "flex size-6 items-center justify-center bg-primary/10 text-xs font-semibold text-primary", children: activeAdministrator.firstName.charAt(0).toUpperCase() }), _jsxs("div", { className: "truncate text-sm", children: [activeAdministrator.firstName, " ", activeAdministrator.lastName] })] }), _jsx(DropdownMenuSeparator, { className: "my-1" })] })), reindexEnabled && (_jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2 text-nowrap", onSelect: rebuildSearchIndex, children: [_jsx(RotateCwSquare, { className: "size-4" }), t('rebuildSerachIndex')] })), reindexEnabled && fastLinks.length > 0 && _jsx(DropdownMenuSeparator, {}), fastLinks.length > 0 && (_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { children: t('fastLinks') }), _jsx(DropdownMenuPortal, { children: _jsx(DropdownMenuSubContent, { children: fastLinks.map((link, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2 text-nowrap", onSelect: () => navigate(link.path, { viewTransition: true }), children: [_jsx(RotateCwSquare, { className: "size-4" }), link.label] })] }, link.key))) }) })] }) })), allowedTopNavigationActions?.length && allowedTopNavigationActions.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), allowedTopNavigationActions.map((action) => (_jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2", onSelect: action.onClick, children: [action.icon && _jsx(action.icon, { className: "size-4" }), action.label] }, action.label)))] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "flex cursor-pointer items-center gap-2 text-red-500", onSelect: async () => {
55
89
  const result = await createDialog({
56
90
  title: t('logOutConfirmation'),
57
91
  description: t('logOutConfirmationDescription'),
@@ -60,7 +94,9 @@ export const Menu = ({ children }) => {
60
94
  { label: t('logOut'), variant: 'destructive', returnValue: true },
61
95
  ],
62
96
  });
63
- if (result)
97
+ if (result) {
98
+ clearAdministratorAccess();
64
99
  logOut();
100
+ }
65
101
  }, children: [_jsx(LogOutIcon, { className: "size-4" }), t('logOut')] })] })] })] })] }), _jsx(ScrollArea, { className: "relative h-[calc(100vh-64px)] overflow-y-hidden lg:h-[calc(100vh-72px)]", children: children })] })] }) }) }) }) }));
66
102
  };
@@ -32,6 +32,68 @@
32
32
  "placeholder": "Adding roles to a user"
33
33
  }
34
34
  },
35
+ "provision": {
36
+ "action": "Create with role",
37
+ "title": "Create administrator with role",
38
+ "description": "Use a business preset to create a role and assign it to a new administrator in one flow.",
39
+ "submit": "Create administrator",
40
+ "submitting": "Creating...",
41
+ "admin": {
42
+ "title": "Administrator account",
43
+ "description": "Credentials and contact details for the new administrator."
44
+ },
45
+ "role": {
46
+ "title": "Role setup",
47
+ "description": "Create a new role from the selected preset or reuse an existing role by code.",
48
+ "code": "Role code",
49
+ "descriptionLabel": "Role name",
50
+ "reuseExisting": "Use existing role with this code",
51
+ "reuseExistingHint": "When enabled, permissions and channels below are ignored and the administrator gets the existing role.",
52
+ "channels": "Channels",
53
+ "channelsPlaceholder": "Add channel"
54
+ },
55
+ "permissions": {
56
+ "title": "Permissions",
57
+ "description": "Review the preset and tweak permissions before creating the role.",
58
+ "reuseExisting": "Permissions are taken from the existing role when reuse mode is enabled."
59
+ },
60
+ "presets": {
61
+ "title": "Business preset",
62
+ "description": "Start from a safe operational role and adjust it if needed.",
63
+ "defaultBadge": "Default",
64
+ "backendNote": "The backend still enforces every permission. This wizard only provisions roles and administrator accounts.",
65
+ "shopManager": {
66
+ "title": "Shop manager",
67
+ "description": "Order handling, customer support, catalog visibility, payment/shipping lookup and stock-location visibility.",
68
+ "roleDescription": "Shop manager"
69
+ }
70
+ },
71
+ "summary": {
72
+ "title": "Summary",
73
+ "description": "What will be submitted.",
74
+ "role": "Role",
75
+ "channels": "Channels",
76
+ "permissions": "Permissions",
77
+ "mode": "Mode",
78
+ "create": "Create role",
79
+ "reuse": "Reuse role"
80
+ },
81
+ "validation": {
82
+ "emailRequired": "Email is required",
83
+ "emailInvalid": "Enter a valid email address",
84
+ "roleCodeRequired": "Role code is required",
85
+ "channelsRequired": "Select at least one channel",
86
+ "permissionsRequired": "Select at least one permission",
87
+ "roleCodeExists": "A role with this code already exists. Enable reuse or choose another code.",
88
+ "roleNotFound": "No existing role with this code was found."
89
+ },
90
+ "toasts": {
91
+ "success": "Administrator and role were created successfully",
92
+ "error": "Could not create administrator with role",
93
+ "roleCodeExists": "A role with this code already exists",
94
+ "roleNotFound": "No existing role with this code was found"
95
+ }
96
+ },
35
97
  "toasts": {
36
98
  "adminLoadingError": "Failed to load administrator with ID {{value}}",
37
99
  "adminUpdateSuccess": "Administrator successfully updated",
@@ -582,6 +582,7 @@
582
582
  "globalSettings": "global settings",
583
583
  "roles": "roles",
584
584
  "admins": "administrators",
585
+ "adminProvision": "administrator provisioning",
585
586
  "stock": "storage locations",
586
587
  "sellers": "retailers",
587
588
  "assets": "assets",
@@ -32,6 +32,68 @@
32
32
  "placeholder": "Dodawanie ról do użytkownika"
33
33
  }
34
34
  },
35
+ "provision": {
36
+ "action": "Utwórz z rolą",
37
+ "title": "Utwórz administratora z rolą",
38
+ "description": "Użyj biznesowego presetu, żeby w jednym przepływie utworzyć rolę i przypisać ją do nowego administratora.",
39
+ "submit": "Utwórz administratora",
40
+ "submitting": "Tworzenie...",
41
+ "admin": {
42
+ "title": "Konto administratora",
43
+ "description": "Dane logowania i kontaktowe nowego administratora."
44
+ },
45
+ "role": {
46
+ "title": "Konfiguracja roli",
47
+ "description": "Utwórz nową rolę z wybranego presetu albo użyj istniejącej roli po kodzie.",
48
+ "code": "Kod roli",
49
+ "descriptionLabel": "Nazwa roli",
50
+ "reuseExisting": "Użyj istniejącej roli o tym kodzie",
51
+ "reuseExistingHint": "Po włączeniu tej opcji uprawnienia i kanały poniżej są ignorowane, a administrator otrzyma istniejącą rolę.",
52
+ "channels": "Kanały",
53
+ "channelsPlaceholder": "Dodaj kanał"
54
+ },
55
+ "permissions": {
56
+ "title": "Uprawnienia",
57
+ "description": "Przejrzyj preset i dostosuj uprawnienia przed utworzeniem roli.",
58
+ "reuseExisting": "Uprawnienia są pobierane z istniejącej roli, gdy włączone jest użycie istniejącej roli."
59
+ },
60
+ "presets": {
61
+ "title": "Preset biznesowy",
62
+ "description": "Zacznij od bezpiecznej roli operacyjnej i dostosuj ją, jeśli trzeba.",
63
+ "defaultBadge": "Domyślny",
64
+ "backendNote": "Backend nadal egzekwuje każde uprawnienie. Ten wizard tylko zakłada role i konta administratorów.",
65
+ "shopManager": {
66
+ "title": "Shop manager",
67
+ "description": "Obsługa zamówień, wsparcie klienta, widoczność katalogu, podgląd płatności/wysyłki i lokalizacji magazynowych.",
68
+ "roleDescription": "Shop manager"
69
+ }
70
+ },
71
+ "summary": {
72
+ "title": "Podsumowanie",
73
+ "description": "To zostanie wysłane.",
74
+ "role": "Rola",
75
+ "channels": "Kanały",
76
+ "permissions": "Uprawnienia",
77
+ "mode": "Tryb",
78
+ "create": "Utwórz rolę",
79
+ "reuse": "Użyj roli"
80
+ },
81
+ "validation": {
82
+ "emailRequired": "Adres e-mail jest wymagany",
83
+ "emailInvalid": "Podaj poprawny adres e-mail",
84
+ "roleCodeRequired": "Kod roli jest wymagany",
85
+ "channelsRequired": "Wybierz co najmniej jeden kanał",
86
+ "permissionsRequired": "Wybierz co najmniej jedno uprawnienie",
87
+ "roleCodeExists": "Rola o tym kodzie już istnieje. Włącz użycie istniejącej roli albo wybierz inny kod.",
88
+ "roleNotFound": "Nie znaleziono istniejącej roli o tym kodzie."
89
+ },
90
+ "toasts": {
91
+ "success": "Administrator i rola zostały utworzone pomyślnie",
92
+ "error": "Nie udało się utworzyć administratora z rolą",
93
+ "roleCodeExists": "Rola o tym kodzie już istnieje",
94
+ "roleNotFound": "Nie znaleziono istniejącej roli o tym kodzie"
95
+ }
96
+ },
35
97
  "toasts": {
36
98
  "adminLoadingError": "Nie udało się załadować administratora o identyfikatorze {{value}}",
37
99
  "adminUpdateSuccess": "Administrator został pomyślnie zaktualizowany",