@hed-hog/core 0.0.302 → 0.0.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/menu/menu.service.d.ts +1 -0
- package/dist/menu/menu.service.d.ts.map +1 -1
- package/dist/menu/menu.service.js +65 -0
- package/dist/menu/menu.service.js.map +1 -1
- package/hedhog/frontend/app/menu/page.tsx.ejs +1924 -751
- package/hedhog/frontend/app/roles/menus.tsx.ejs +45 -28
- package/hedhog/frontend/app/roles/routes.tsx.ejs +21 -16
- package/hedhog/frontend/app/roles/users.tsx.ejs +22 -14
- package/hedhog/frontend/app/users/permissions.tsx.ejs +7 -9
- package/hedhog/frontend/messages/en.json +28 -1
- package/hedhog/frontend/messages/pt.json +28 -1
- package/hedhog/table/role_menu.yaml +0 -2
- package/package.json +3 -3
- package/src/menu/menu.service.ts +100 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { EmptyState } from '@/components/entity-list';
|
|
3
4
|
import {
|
|
4
5
|
Accordion,
|
|
5
6
|
AccordionContent,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
Menu as MenuIcon,
|
|
28
29
|
} from 'lucide-react';
|
|
29
30
|
import { useTranslations } from 'next-intl';
|
|
30
|
-
import { JSX, useEffect, useState } from 'react';
|
|
31
|
+
import { JSX, useEffect, useMemo, useState, type ComponentType } from 'react';
|
|
31
32
|
import { toast } from 'sonner';
|
|
32
33
|
|
|
33
34
|
type Menu = {
|
|
@@ -51,6 +52,8 @@ type RoleMenusSectionProps = {
|
|
|
51
52
|
onMenuChange?: () => void;
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
const EMPTY_MENUS: Menu[] = [];
|
|
56
|
+
|
|
54
57
|
export function RoleMenusSection({
|
|
55
58
|
roleId,
|
|
56
59
|
onMenuChange,
|
|
@@ -104,7 +107,7 @@ export function RoleMenusSection({
|
|
|
104
107
|
enabled: !!roleId,
|
|
105
108
|
});
|
|
106
109
|
|
|
107
|
-
const menus = menusData?.data
|
|
110
|
+
const menus = menusData?.data ?? EMPTY_MENUS;
|
|
108
111
|
const totalPages = menusData?.lastPage || 1;
|
|
109
112
|
const totalMenus = menusData?.total || 0;
|
|
110
113
|
|
|
@@ -189,7 +192,7 @@ export function RoleMenusSection({
|
|
|
189
192
|
await refetchAssignedMenus();
|
|
190
193
|
await refetchMenus();
|
|
191
194
|
onMenuChange?.();
|
|
192
|
-
} catch
|
|
195
|
+
} catch {
|
|
193
196
|
toast.error(
|
|
194
197
|
isAssigned ? t('errorRemovingMenu') : t('errorAssigningMenu')
|
|
195
198
|
);
|
|
@@ -219,19 +222,25 @@ export function RoleMenusSection({
|
|
|
219
222
|
match.replace('-', '').toUpperCase()
|
|
220
223
|
);
|
|
221
224
|
|
|
225
|
+
const tablerIcons = TablerIcons as Record<
|
|
226
|
+
string,
|
|
227
|
+
ComponentType<{ className?: string }>
|
|
228
|
+
>;
|
|
222
229
|
const pascalName = toPascalCase(String(iconName));
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
const iconCandidates = [
|
|
231
|
+
`Icon${pascalName}`,
|
|
232
|
+
`Icon${pascalName}Filled`,
|
|
233
|
+
`Icon${pascalName}Circle`,
|
|
234
|
+
];
|
|
235
|
+
const IconComponent = iconCandidates
|
|
236
|
+
.map((candidate) => tablerIcons[candidate])
|
|
237
|
+
.find(Boolean);
|
|
238
|
+
|
|
239
|
+
return IconComponent ? (
|
|
240
|
+
<IconComponent className="h-5 w-5" />
|
|
241
|
+
) : (
|
|
242
|
+
<MenuIcon className="h-5 w-5" />
|
|
243
|
+
);
|
|
235
244
|
};
|
|
236
245
|
|
|
237
246
|
const organizeMenuHierarchy = (menusList: Menu[]): Menu[] => {
|
|
@@ -255,15 +264,23 @@ export function RoleMenusSection({
|
|
|
255
264
|
return rootMenus;
|
|
256
265
|
};
|
|
257
266
|
|
|
258
|
-
const hierarchicalMenus =
|
|
267
|
+
const hierarchicalMenus = useMemo(
|
|
268
|
+
() => organizeMenuHierarchy(menus),
|
|
269
|
+
[menus]
|
|
270
|
+
);
|
|
271
|
+
const defaultExpandedMenus = useMemo(
|
|
272
|
+
() =>
|
|
273
|
+
hierarchicalMenus
|
|
274
|
+
.filter((menu) => menu.children && menu.children.length > 0)
|
|
275
|
+
.map((menu) => `menu-${menu.id}`),
|
|
276
|
+
[hierarchicalMenus]
|
|
277
|
+
);
|
|
278
|
+
|
|
259
279
|
useEffect(() => {
|
|
260
|
-
if (expandedMenus.length === 0 &&
|
|
261
|
-
|
|
262
|
-
.filter((m) => m.children && m.children.length > 0)
|
|
263
|
-
.map((m) => `menu-${m.id}`);
|
|
264
|
-
setExpandedMenus(allParentIds);
|
|
280
|
+
if (expandedMenus.length === 0 && defaultExpandedMenus.length > 0) {
|
|
281
|
+
setExpandedMenus(defaultExpandedMenus);
|
|
265
282
|
}
|
|
266
|
-
}, [
|
|
283
|
+
}, [defaultExpandedMenus, expandedMenus.length]);
|
|
267
284
|
|
|
268
285
|
const renderChildMenu = (menu: Menu): JSX.Element => {
|
|
269
286
|
const isAssigned = isMenuAssigned(menu);
|
|
@@ -326,12 +343,12 @@ export function RoleMenusSection({
|
|
|
326
343
|
|
|
327
344
|
if (!menus || menus.length === 0) {
|
|
328
345
|
return (
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
346
|
+
<EmptyState
|
|
347
|
+
className="min-h-60 py-10"
|
|
348
|
+
icon={<MenuIcon className="h-12 w-12" />}
|
|
349
|
+
title={t('noMenusAvailable')}
|
|
350
|
+
description={t('menusDescription')}
|
|
351
|
+
/>
|
|
335
352
|
);
|
|
336
353
|
}
|
|
337
354
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { EmptyState } from '@/components/entity-list';
|
|
3
4
|
import { Button } from '@/components/ui/button';
|
|
4
5
|
import { Card, CardContent } from '@/components/ui/card';
|
|
5
6
|
import { Input } from '@/components/ui/input';
|
|
@@ -43,6 +44,8 @@ type RoleRoutesSectionProps = {
|
|
|
43
44
|
onRouteChange?: () => void;
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
type SearchType = 'contains' | 'startsWith' | 'endsWith';
|
|
48
|
+
|
|
46
49
|
export function RoleRoutesSection({
|
|
47
50
|
roleId,
|
|
48
51
|
onRouteChange,
|
|
@@ -53,12 +56,20 @@ export function RoleRoutesSection({
|
|
|
53
56
|
const [page, setPage] = useState(1);
|
|
54
57
|
const [pageSize, setPageSize] = useState(10);
|
|
55
58
|
const [searchTerm, setSearchTerm] = useState('');
|
|
56
|
-
const [searchType, setSearchType] = useState<
|
|
57
|
-
'contains' | 'startsWith' | 'endsWith'
|
|
58
|
-
>('contains');
|
|
59
|
+
const [searchType, setSearchType] = useState<SearchType>('contains');
|
|
59
60
|
const [methodFilter, setMethodFilter] = useState<string>('all');
|
|
60
61
|
const debouncedSearch = useDebounce(searchTerm);
|
|
61
62
|
|
|
63
|
+
const handleSearchTypeChange = (value: string) => {
|
|
64
|
+
if (
|
|
65
|
+
value === 'contains' ||
|
|
66
|
+
value === 'startsWith' ||
|
|
67
|
+
value === 'endsWith'
|
|
68
|
+
) {
|
|
69
|
+
setSearchType(value);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
62
73
|
const {
|
|
63
74
|
data: assignedRoutesData,
|
|
64
75
|
isLoading: isLoadingAssigned,
|
|
@@ -143,7 +154,7 @@ export function RoleRoutesSection({
|
|
|
143
154
|
await refetchAssignedRoutes();
|
|
144
155
|
await refetchRoutes();
|
|
145
156
|
onRouteChange?.();
|
|
146
|
-
} catch
|
|
157
|
+
} catch {
|
|
147
158
|
toast.error(
|
|
148
159
|
isAssigned ? t('errorRemovingRoute') : t('errorAssigningRoute')
|
|
149
160
|
);
|
|
@@ -203,10 +214,7 @@ export function RoleRoutesSection({
|
|
|
203
214
|
/>
|
|
204
215
|
</div>
|
|
205
216
|
<div className="w-full flex gap-2">
|
|
206
|
-
<Select
|
|
207
|
-
value={searchType}
|
|
208
|
-
onValueChange={(value: any) => setSearchType(value)}
|
|
209
|
-
>
|
|
217
|
+
<Select value={searchType} onValueChange={handleSearchTypeChange}>
|
|
210
218
|
<SelectTrigger className="w-full">
|
|
211
219
|
<SelectValue placeholder={t('selectSearchType')} />
|
|
212
220
|
</SelectTrigger>
|
|
@@ -299,14 +307,11 @@ export function RoleRoutesSection({
|
|
|
299
307
|
);
|
|
300
308
|
})
|
|
301
309
|
) : (
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
</p>
|
|
308
|
-
</CardContent>
|
|
309
|
-
</Card>
|
|
310
|
+
<EmptyState
|
|
311
|
+
className="min-h-60 py-10"
|
|
312
|
+
icon={<RouteIcon className="h-12 w-12" />}
|
|
313
|
+
title={t('noRoutesAvailable')}
|
|
314
|
+
/>
|
|
310
315
|
)}
|
|
311
316
|
</div>
|
|
312
317
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { EmptyState } from '@/components/entity-list';
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
3
5
|
import { Button } from '@/components/ui/button';
|
|
4
6
|
import { Card, CardContent } from '@/components/ui/card';
|
|
5
7
|
import { Input } from '@/components/ui/input';
|
|
@@ -120,7 +122,7 @@ export function RoleUsersSection({
|
|
|
120
122
|
await refetchAssignedUsers();
|
|
121
123
|
await refetchAllUsers();
|
|
122
124
|
onUserChange?.();
|
|
123
|
-
} catch
|
|
125
|
+
} catch {
|
|
124
126
|
toast.error(
|
|
125
127
|
isAssigned ? t('errorRemovingUser') : t('errorAssigningUser')
|
|
126
128
|
);
|
|
@@ -184,11 +186,20 @@ export function RoleUsersSection({
|
|
|
184
186
|
>
|
|
185
187
|
<CardContent className="flex items-center justify-between">
|
|
186
188
|
<div className="flex items-center gap-3 flex-1">
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
<Avatar className="h-10 w-10">
|
|
190
|
+
<AvatarImage
|
|
191
|
+
src={getPhotoUrl(user.photo_id)}
|
|
192
|
+
alt={user.name}
|
|
193
|
+
/>
|
|
194
|
+
<AvatarFallback>
|
|
195
|
+
{user.name
|
|
196
|
+
?.split(' ')
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.slice(0, 2)
|
|
199
|
+
.map((part) => part[0]?.toUpperCase() ?? '')
|
|
200
|
+
.join('') || 'U'}
|
|
201
|
+
</AvatarFallback>
|
|
202
|
+
</Avatar>
|
|
192
203
|
<div className="flex-1">
|
|
193
204
|
<Label
|
|
194
205
|
htmlFor={`user-${user.id}`}
|
|
@@ -215,14 +226,11 @@ export function RoleUsersSection({
|
|
|
215
226
|
);
|
|
216
227
|
})
|
|
217
228
|
) : (
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
</p>
|
|
224
|
-
</CardContent>
|
|
225
|
-
</Card>
|
|
229
|
+
<EmptyState
|
|
230
|
+
className="min-h-60 py-10"
|
|
231
|
+
icon={<UserCircle className="h-12 w-12" />}
|
|
232
|
+
title={t('noUsersAvailable')}
|
|
233
|
+
/>
|
|
226
234
|
)}
|
|
227
235
|
</div>
|
|
228
236
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { EmptyState } from '@/components/entity-list';
|
|
3
4
|
import { Card, CardContent } from '@/components/ui/card';
|
|
4
5
|
import { Label } from '@/components/ui/label';
|
|
5
6
|
import { Switch } from '@/components/ui/switch';
|
|
@@ -61,7 +62,7 @@ export function PermissionsSection({
|
|
|
61
62
|
}
|
|
62
63
|
await refetchUserRoles();
|
|
63
64
|
onRoleChange?.();
|
|
64
|
-
} catch
|
|
65
|
+
} catch {
|
|
65
66
|
toast.error(
|
|
66
67
|
isAssigned ? t('errorRemovingRole') : t('errorAssigningRole')
|
|
67
68
|
);
|
|
@@ -83,14 +84,11 @@ export function PermissionsSection({
|
|
|
83
84
|
|
|
84
85
|
if (!userRoles || userRoles.length === 0) {
|
|
85
86
|
return (
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</p>
|
|
92
|
-
</CardContent>
|
|
93
|
-
</Card>
|
|
87
|
+
<EmptyState
|
|
88
|
+
className="min-h-60 py-10"
|
|
89
|
+
icon={<ShieldCheck className="h-12 w-12" />}
|
|
90
|
+
title={t('noRolesAvailable')}
|
|
91
|
+
/>
|
|
94
92
|
);
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -1484,6 +1484,33 @@
|
|
|
1484
1484
|
"treeOrderSaved": "Menu order saved successfully.",
|
|
1485
1485
|
"treeOrderError": "Error saving menu order.",
|
|
1486
1486
|
"treeChild": "child",
|
|
1487
|
-
"treeChildren": "children"
|
|
1487
|
+
"treeChildren": "children",
|
|
1488
|
+
"refresh": "Refresh",
|
|
1489
|
+
"treeWorkspaceTitle": "Structure and permissions",
|
|
1490
|
+
"treeWorkspaceDescription": "Click to select, drag to reorganize, and use the context menu for quick actions.",
|
|
1491
|
+
"treeEmptySearch": "No menu matches the current search.",
|
|
1492
|
+
"selectedMenuTitle": "Selected menu",
|
|
1493
|
+
"selectedMenuDescription": "Edit the item, organize its hierarchy, and define which roles unlock access.",
|
|
1494
|
+
"noMenuSelectedTitle": "Select a menu to get started",
|
|
1495
|
+
"noMenuSelectedDescription": "Choose an item in the tree to edit its data and manage permissions without leaving the page.",
|
|
1496
|
+
"duplicateMenu": "Duplicate menu",
|
|
1497
|
+
"duplicateSuccess": "Menu duplicated successfully.",
|
|
1498
|
+
"duplicateError": "Unable to duplicate the menu.",
|
|
1499
|
+
"moveToRoot": "Move to root",
|
|
1500
|
+
"moveToRootSuccess": "Menu moved to the root.",
|
|
1501
|
+
"addSubmenu": "Add submenu",
|
|
1502
|
+
"rootMenuLabel": "Root",
|
|
1503
|
+
"childrenCount": "Children",
|
|
1504
|
+
"parentMenuSummary": "Parent menu",
|
|
1505
|
+
"selectedLocaleLabel": "Language",
|
|
1506
|
+
"openTreeOnMobile": "Open tree",
|
|
1507
|
+
"totalSubmenus": "Submenus",
|
|
1508
|
+
"searchResults": "Results",
|
|
1509
|
+
"usersWithAccessTitle": "Users with access",
|
|
1510
|
+
"usersWithAccessDescription": "Preview of who can open this menu through the assigned roles.",
|
|
1511
|
+
"noAssignedRolesPreview": "Assign at least one role to preview who will gain access.",
|
|
1512
|
+
"noUsersWithAccess": "No users are currently linked to the assigned roles.",
|
|
1513
|
+
"grantedByRole": "Granted by",
|
|
1514
|
+
"moreUsers": "+{count} more users"
|
|
1488
1515
|
}
|
|
1489
1516
|
}
|
|
@@ -1539,6 +1539,33 @@
|
|
|
1539
1539
|
"treeOrderSaved": "Ordem dos menus salva com sucesso.",
|
|
1540
1540
|
"treeOrderError": "Erro ao salvar a ordem dos menus.",
|
|
1541
1541
|
"treeChild": "filho",
|
|
1542
|
-
"treeChildren": "filhos"
|
|
1542
|
+
"treeChildren": "filhos",
|
|
1543
|
+
"refresh": "Atualizar",
|
|
1544
|
+
"treeWorkspaceTitle": "Estrutura e permissões",
|
|
1545
|
+
"treeWorkspaceDescription": "Clique para selecionar, arraste para reorganizar e use o menu de contexto para ações rápidas.",
|
|
1546
|
+
"treeEmptySearch": "Nenhum menu corresponde à busca atual.",
|
|
1547
|
+
"selectedMenuTitle": "Menu selecionado",
|
|
1548
|
+
"selectedMenuDescription": "Edite o item, organize sua hierarquia e defina quais cargos liberam o acesso.",
|
|
1549
|
+
"noMenuSelectedTitle": "Selecione um menu para começar",
|
|
1550
|
+
"noMenuSelectedDescription": "Escolha um item na árvore para editar seus dados e controlar suas permissões sem sair da tela.",
|
|
1551
|
+
"duplicateMenu": "Duplicar menu",
|
|
1552
|
+
"duplicateSuccess": "Menu duplicado com sucesso.",
|
|
1553
|
+
"duplicateError": "Não foi possível duplicar o menu.",
|
|
1554
|
+
"moveToRoot": "Mover para raiz",
|
|
1555
|
+
"moveToRootSuccess": "Menu movido para a raiz.",
|
|
1556
|
+
"addSubmenu": "Adicionar submenu",
|
|
1557
|
+
"rootMenuLabel": "Raiz",
|
|
1558
|
+
"childrenCount": "Filhos",
|
|
1559
|
+
"parentMenuSummary": "Menu pai",
|
|
1560
|
+
"selectedLocaleLabel": "Idioma",
|
|
1561
|
+
"openTreeOnMobile": "Abrir árvore",
|
|
1562
|
+
"totalSubmenus": "Submenus",
|
|
1563
|
+
"searchResults": "Resultados",
|
|
1564
|
+
"usersWithAccessTitle": "Usuários com acesso",
|
|
1565
|
+
"usersWithAccessDescription": "Prévia de quem poderá abrir este menu por causa dos cargos atribuídos.",
|
|
1566
|
+
"noAssignedRolesPreview": "Associe pelo menos um cargo para visualizar quem ganhará acesso.",
|
|
1567
|
+
"noUsersWithAccess": "Nenhum usuário está vinculado aos cargos atribuídos no momento.",
|
|
1568
|
+
"grantedByRole": "Liberado por",
|
|
1569
|
+
"moreUsers": "+{count} usuários"
|
|
1543
1570
|
}
|
|
1544
1571
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.303",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"speakeasy": "^2.0.0",
|
|
32
32
|
"uuid": "^11.1.0",
|
|
33
33
|
"@hed-hog/api": "0.0.6",
|
|
34
|
+
"@hed-hog/api-locale": "0.0.14",
|
|
34
35
|
"@hed-hog/api-mail": "0.0.9",
|
|
35
|
-
"@hed-hog/api-types": "0.0.1",
|
|
36
36
|
"@hed-hog/api-prisma": "0.0.6",
|
|
37
|
-
"@hed-hog/api-
|
|
37
|
+
"@hed-hog/api-types": "0.0.1",
|
|
38
38
|
"@hed-hog/api-pagination": "0.0.7"
|
|
39
39
|
},
|
|
40
40
|
"exports": {
|
package/src/menu/menu.service.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { getLocaleText } from '@hed-hog/api-locale';
|
|
|
3
3
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
4
4
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
BadRequestException,
|
|
7
|
+
Inject,
|
|
8
|
+
Injectable,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
forwardRef,
|
|
11
11
|
} from '@nestjs/common';
|
|
12
12
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
13
13
|
import { UpdateIdsDTO } from '../dto/update-ids.dto';
|
|
@@ -24,6 +24,67 @@ export class MenuService {
|
|
|
24
24
|
private readonly paginationService: PaginationService,
|
|
25
25
|
) {}
|
|
26
26
|
|
|
27
|
+
private async ensureValidParent(
|
|
28
|
+
locale: string,
|
|
29
|
+
currentMenuId: number | null,
|
|
30
|
+
parentId?: number | null,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (parentId == null) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parent = await this.prismaService.menu.findUnique({
|
|
37
|
+
where: { id: parentId },
|
|
38
|
+
select: { id: true, menu_id: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!parent) {
|
|
42
|
+
throw new BadRequestException(
|
|
43
|
+
getLocaleText('menuNotFound', locale, 'Menu not found.'),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (currentMenuId != null && parentId === currentMenuId) {
|
|
48
|
+
throw new BadRequestException(
|
|
49
|
+
getLocaleText(
|
|
50
|
+
'menuInvalidParent',
|
|
51
|
+
locale,
|
|
52
|
+
'A menu cannot be its own parent.',
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (currentMenuId == null) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const visited = new Set<number>([currentMenuId]);
|
|
62
|
+
let cursor: { id: number; menu_id: number | null } | null = parent;
|
|
63
|
+
|
|
64
|
+
while (cursor) {
|
|
65
|
+
if (visited.has(cursor.id)) {
|
|
66
|
+
throw new BadRequestException(
|
|
67
|
+
getLocaleText(
|
|
68
|
+
'menuInvalidParent',
|
|
69
|
+
locale,
|
|
70
|
+
'You cannot move a menu inside itself or one of its descendants.',
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visited.add(cursor.id);
|
|
76
|
+
|
|
77
|
+
if (cursor.menu_id == null) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cursor = await this.prismaService.menu.findUnique({
|
|
82
|
+
where: { id: cursor.menu_id },
|
|
83
|
+
select: { id: true, menu_id: true },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
27
88
|
async updateScreens(locale:string,menuId: number, data: UpdateIdsDTO): Promise<{count:number}> {
|
|
28
89
|
|
|
29
90
|
const menuExists = await this.prismaService.menu.count({
|
|
@@ -170,6 +231,26 @@ export class MenuService {
|
|
|
170
231
|
menu_id: true,
|
|
171
232
|
},
|
|
172
233
|
},
|
|
234
|
+
role_user: {
|
|
235
|
+
select: {
|
|
236
|
+
user_id: true,
|
|
237
|
+
user: {
|
|
238
|
+
select: {
|
|
239
|
+
id: true,
|
|
240
|
+
name: true,
|
|
241
|
+
user_identifier: {
|
|
242
|
+
where: {
|
|
243
|
+
type: 'email',
|
|
244
|
+
},
|
|
245
|
+
select: {
|
|
246
|
+
value: true,
|
|
247
|
+
},
|
|
248
|
+
take: 1,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
173
254
|
},
|
|
174
255
|
},
|
|
175
256
|
'role_locale',
|
|
@@ -252,6 +333,11 @@ export class MenuService {
|
|
|
252
333
|
where: { locale: { code: locale } },
|
|
253
334
|
select: { name: true },
|
|
254
335
|
},
|
|
336
|
+
_count: {
|
|
337
|
+
select: {
|
|
338
|
+
role_menu: true,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
255
341
|
},
|
|
256
342
|
});
|
|
257
343
|
return menus.map((m: any) => itemTranslations('menu_locale', m));
|
|
@@ -293,6 +379,11 @@ export class MenuService {
|
|
|
293
379
|
name: true,
|
|
294
380
|
},
|
|
295
381
|
},
|
|
382
|
+
_count: {
|
|
383
|
+
select: {
|
|
384
|
+
role_menu: true,
|
|
385
|
+
},
|
|
386
|
+
},
|
|
296
387
|
},
|
|
297
388
|
},
|
|
298
389
|
'menu_locale',
|
|
@@ -323,6 +414,8 @@ export class MenuService {
|
|
|
323
414
|
}
|
|
324
415
|
|
|
325
416
|
async create(_locale: string, { slug, url, icon, order, menu_id, locale }: CreateDTO) {
|
|
417
|
+
await this.ensureValidParent(_locale, null, menu_id);
|
|
418
|
+
|
|
326
419
|
const created = await this.prismaService.menu.create({
|
|
327
420
|
data: { slug, url, icon, order, menu_id },
|
|
328
421
|
});
|
|
@@ -355,6 +448,8 @@ export class MenuService {
|
|
|
355
448
|
},
|
|
356
449
|
});
|
|
357
450
|
|
|
451
|
+
await this.ensureValidParent(locale, id, data.menu_id);
|
|
452
|
+
|
|
358
453
|
if (!menuExists) {
|
|
359
454
|
throw new BadRequestException(
|
|
360
455
|
getLocaleText('menuNotFound', locale, 'Menu not found.'),
|