@hed-hog/core 0.0.321 → 0.0.322
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/auth/auth.controller.d.ts +3 -3
- package/dist/auth/auth.service.d.ts +8 -8
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
- package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
- package/dist/file/file.controller.d.ts +2 -2
- package/dist/file/file.service.d.ts +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mail-profile/dto/create.dto.d.ts +23 -0
- package/dist/mail-profile/dto/create.dto.d.ts.map +1 -0
- package/dist/mail-profile/dto/create.dto.js +139 -0
- package/dist/mail-profile/dto/create.dto.js.map +1 -0
- package/dist/mail-profile/dto/update.dto.d.ts +6 -0
- package/dist/mail-profile/dto/update.dto.d.ts.map +1 -0
- package/dist/mail-profile/dto/update.dto.js +9 -0
- package/dist/mail-profile/dto/update.dto.js.map +1 -0
- package/dist/mail-profile/mail-profile.controller.d.ts +94 -0
- package/dist/mail-profile/mail-profile.controller.d.ts.map +1 -0
- package/dist/mail-profile/mail-profile.controller.js +95 -0
- package/dist/mail-profile/mail-profile.controller.js.map +1 -0
- package/dist/mail-profile/mail-profile.module.d.ts +3 -0
- package/dist/mail-profile/mail-profile.module.d.ts.map +1 -0
- package/dist/mail-profile/mail-profile.module.js +31 -0
- package/dist/mail-profile/mail-profile.module.js.map +1 -0
- package/dist/mail-profile/mail-profile.service.d.ts +109 -0
- package/dist/mail-profile/mail-profile.service.d.ts.map +1 -0
- package/dist/mail-profile/mail-profile.service.js +278 -0
- package/dist/mail-profile/mail-profile.service.js.map +1 -0
- package/dist/session/session.controller.d.ts +1 -1
- package/dist/session/session.service.d.ts +3 -3
- package/dist/user/user.controller.d.ts +18 -3
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +22 -0
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +22 -7
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +99 -0
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/menu.yaml +35 -3
- package/hedhog/data/route.yaml +64 -0
- package/hedhog/frontend/app/mail/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +828 -0
- package/hedhog/frontend/app/users/page.tsx.ejs +25 -1383
- package/hedhog/frontend/app/users/permissions.tsx.ejs +109 -55
- package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +304 -0
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +1385 -0
- package/hedhog/frontend/messages/en.json +91 -0
- package/hedhog/frontend/messages/pt.json +91 -0
- package/hedhog/table/mail_profile.yaml +75 -0
- package/package.json +4 -4
- package/src/core.module.ts +3 -0
- package/src/index.ts +2 -0
- package/src/mail-profile/dto/create.dto.ts +145 -0
- package/src/mail-profile/dto/update.dto.ts +4 -0
- package/src/mail-profile/mail-profile.controller.ts +56 -0
- package/src/mail-profile/mail-profile.module.ts +18 -0
- package/src/mail-profile/mail-profile.service.ts +308 -0
- package/src/user/user.controller.ts +13 -0
- package/src/user/user.service.ts +98 -5
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { EmptyState } from '@/components/entity-list';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
4
5
|
import { Card, CardContent } from '@/components/ui/card';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
5
7
|
import { Label } from '@/components/ui/label';
|
|
6
8
|
import { Switch } from '@/components/ui/switch';
|
|
7
9
|
import { Role } from '@hed-hog/api-types';
|
|
8
10
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
9
|
-
import { Loader2, ShieldCheck } from 'lucide-react';
|
|
11
|
+
import { Eye, Loader2, Search, ShieldCheck } from 'lucide-react';
|
|
10
12
|
import { useTranslations } from 'next-intl';
|
|
11
13
|
import { useState } from 'react';
|
|
12
14
|
import { toast } from 'sonner';
|
|
15
|
+
import { UserAccessSheet } from './user-access-sheet';
|
|
13
16
|
|
|
14
17
|
type CustomRole = Role & {
|
|
15
18
|
isAssigned: boolean;
|
|
@@ -17,16 +20,20 @@ type CustomRole = Role & {
|
|
|
17
20
|
|
|
18
21
|
type PermissionsSectionProps = {
|
|
19
22
|
userId: number;
|
|
23
|
+
userName?: string;
|
|
20
24
|
onRoleChange?: () => void;
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
export function PermissionsSection({
|
|
24
28
|
userId,
|
|
29
|
+
userName,
|
|
25
30
|
onRoleChange,
|
|
26
31
|
}: PermissionsSectionProps) {
|
|
27
32
|
const t = useTranslations('core.UserPage');
|
|
28
33
|
const { request, currentLocaleCode } = useApp();
|
|
29
34
|
const [togglingRoleId, setTogglingRoleId] = useState<number | null>(null);
|
|
35
|
+
const [search, setSearch] = useState('');
|
|
36
|
+
const [accessSheetOpen, setAccessSheetOpen] = useState(false);
|
|
30
37
|
|
|
31
38
|
const {
|
|
32
39
|
data: userRoles = [],
|
|
@@ -71,6 +78,16 @@ export function PermissionsSection({
|
|
|
71
78
|
}
|
|
72
79
|
};
|
|
73
80
|
|
|
81
|
+
const filteredRoles = userRoles.filter((role) => {
|
|
82
|
+
if (!search.trim()) return true;
|
|
83
|
+
const q = search.toLowerCase();
|
|
84
|
+
return (
|
|
85
|
+
role.name?.toLowerCase().includes(q) ||
|
|
86
|
+
role.slug?.toLowerCase().includes(q) ||
|
|
87
|
+
role.description?.toLowerCase().includes(q)
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
74
91
|
if (isLoadingUserRoles) {
|
|
75
92
|
return (
|
|
76
93
|
<div className="flex items-center justify-center py-8">
|
|
@@ -93,61 +110,98 @@ export function PermissionsSection({
|
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
return (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
<>
|
|
114
|
+
<div className="flex items-center gap-2 mb-3">
|
|
115
|
+
<div className="relative flex-1">
|
|
116
|
+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
|
117
|
+
<Input
|
|
118
|
+
placeholder={t('filterRolesPlaceholder')}
|
|
119
|
+
value={search}
|
|
120
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
121
|
+
className="pl-8 h-8 text-sm"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
<Button
|
|
125
|
+
variant="outline"
|
|
126
|
+
size="sm"
|
|
127
|
+
className="shrink-0 gap-1.5"
|
|
128
|
+
onClick={() => setAccessSheetOpen(true)}
|
|
129
|
+
>
|
|
130
|
+
<Eye className="h-4 w-4" />
|
|
131
|
+
{t('viewAccessButton')}
|
|
132
|
+
</Button>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{filteredRoles.length === 0 ? (
|
|
136
|
+
<div className="flex flex-col items-center justify-center py-10 text-center">
|
|
137
|
+
<Search className="h-8 w-8 text-muted-foreground/40 mb-2" />
|
|
138
|
+
<p className="text-sm text-muted-foreground">{t('noRolesMatch')}</p>
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
{filteredRoles.map((role, index) => {
|
|
143
|
+
const isToggling = togglingRoleId === role.id;
|
|
99
144
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
return (
|
|
146
|
+
<Card
|
|
147
|
+
key={role.id ?? `role-${index}`}
|
|
148
|
+
className={`transition-all py-4 ${
|
|
149
|
+
role.isAssigned
|
|
150
|
+
? 'border-primary bg-primary/5'
|
|
151
|
+
: 'border-border hover:border-primary/50'
|
|
152
|
+
}`}
|
|
153
|
+
>
|
|
154
|
+
<CardContent className="flex items-center justify-between px-4">
|
|
155
|
+
<div className="flex items-center gap-2 flex-1">
|
|
156
|
+
<div
|
|
157
|
+
className={`rounded-md p-2 ${
|
|
158
|
+
role.isAssigned ? 'bg-primary/10' : 'bg-muted'
|
|
159
|
+
}`}
|
|
160
|
+
>
|
|
161
|
+
<ShieldCheck
|
|
162
|
+
className={`h-5 w-5 ${
|
|
163
|
+
role.isAssigned
|
|
164
|
+
? 'text-primary'
|
|
165
|
+
: 'text-muted-foreground'
|
|
166
|
+
}`}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex-1">
|
|
170
|
+
<Label
|
|
171
|
+
htmlFor={`role-${role.id}`}
|
|
172
|
+
className="text-sm font-medium cursor-pointer"
|
|
173
|
+
>
|
|
174
|
+
{role.name} ({role.slug})
|
|
175
|
+
</Label>
|
|
176
|
+
{role.description && (
|
|
177
|
+
<p className="text-xs text-muted-foreground">
|
|
178
|
+
{role.description}
|
|
179
|
+
</p>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<Switch
|
|
184
|
+
id={`role-${role.id}`}
|
|
185
|
+
checked={role.isAssigned}
|
|
186
|
+
disabled={isToggling}
|
|
187
|
+
onCheckedChange={() =>
|
|
188
|
+
handleToggleRole(role.id!, role.isAssigned)
|
|
189
|
+
}
|
|
190
|
+
className="ml-4"
|
|
120
191
|
/>
|
|
121
|
-
</
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)}
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
<Switch
|
|
139
|
-
id={`role-${role.id}`}
|
|
140
|
-
checked={role.isAssigned}
|
|
141
|
-
disabled={isToggling}
|
|
142
|
-
onCheckedChange={() =>
|
|
143
|
-
handleToggleRole(role.id!, role.isAssigned)
|
|
144
|
-
}
|
|
145
|
-
className="ml-4"
|
|
146
|
-
/>
|
|
147
|
-
</CardContent>
|
|
148
|
-
</Card>
|
|
149
|
-
);
|
|
150
|
-
})}
|
|
151
|
-
</div>
|
|
192
|
+
</CardContent>
|
|
193
|
+
</Card>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
<UserAccessSheet
|
|
200
|
+
open={accessSheetOpen}
|
|
201
|
+
onOpenChange={setAccessSheetOpen}
|
|
202
|
+
userId={userId}
|
|
203
|
+
userName={userName}
|
|
204
|
+
/>
|
|
205
|
+
</>
|
|
152
206
|
);
|
|
153
207
|
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import {
|
|
5
|
+
Sheet,
|
|
6
|
+
SheetContent,
|
|
7
|
+
SheetDescription,
|
|
8
|
+
SheetHeader,
|
|
9
|
+
SheetTitle,
|
|
10
|
+
} from '@/components/ui/sheet';
|
|
11
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
12
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
13
|
+
import { Globe, Loader2, Lock, Menu } from 'lucide-react';
|
|
14
|
+
import { useTranslations } from 'next-intl';
|
|
15
|
+
|
|
16
|
+
type MenuRole = { name: string; slug: string };
|
|
17
|
+
|
|
18
|
+
type UserMenu = {
|
|
19
|
+
id: number;
|
|
20
|
+
menuId: number | null;
|
|
21
|
+
name: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
order?: number;
|
|
25
|
+
roles?: MenuRole[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type MenuNode = UserMenu & { children: MenuNode[] };
|
|
29
|
+
|
|
30
|
+
type UserRoute = {
|
|
31
|
+
id: number;
|
|
32
|
+
url: string;
|
|
33
|
+
method: string;
|
|
34
|
+
roles?: MenuRole[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type UserAccessSheetProps = {
|
|
38
|
+
open: boolean;
|
|
39
|
+
onOpenChange: (open: boolean) => void;
|
|
40
|
+
userId: number;
|
|
41
|
+
userName?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const METHOD_COLORS: Record<string, string> = {
|
|
45
|
+
GET: 'bg-green-50 text-green-700 border-green-200',
|
|
46
|
+
POST: 'bg-blue-50 text-blue-700 border-blue-200',
|
|
47
|
+
PATCH: 'bg-amber-50 text-amber-700 border-amber-200',
|
|
48
|
+
PUT: 'bg-orange-50 text-orange-700 border-orange-200',
|
|
49
|
+
DELETE: 'bg-rose-50 text-rose-700 border-rose-200',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function buildTree(flat: UserMenu[]): MenuNode[] {
|
|
53
|
+
const map = new Map<number, MenuNode>();
|
|
54
|
+
flat.forEach((m) => map.set(m.id, { ...m, children: [] }));
|
|
55
|
+
|
|
56
|
+
const roots: MenuNode[] = [];
|
|
57
|
+
map.forEach((node) => {
|
|
58
|
+
if (node.menuId != null && map.has(node.menuId)) {
|
|
59
|
+
map.get(node.menuId)!.children.push(node);
|
|
60
|
+
} else {
|
|
61
|
+
roots.push(node);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const sortNodes = (nodes: MenuNode[]) => {
|
|
66
|
+
nodes.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
67
|
+
nodes.forEach((n) => sortNodes(n.children));
|
|
68
|
+
};
|
|
69
|
+
sortNodes(roots);
|
|
70
|
+
|
|
71
|
+
return roots;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function MenuIcon({ name, className }: { name?: string; className?: string }) {
|
|
75
|
+
if (name) {
|
|
76
|
+
try {
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
78
|
+
const { [name]: DynamicIcon } = require('lucide-react');
|
|
79
|
+
if (DynamicIcon) return <DynamicIcon className={className} />;
|
|
80
|
+
} catch {
|
|
81
|
+
// fallback below
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return <Menu className={className} />;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function MenuTreeNode({ node, depth }: { node: MenuNode; depth: number }) {
|
|
88
|
+
const isLeaf = node.children.length === 0;
|
|
89
|
+
const indent = depth * 16;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<div
|
|
94
|
+
className="flex items-center gap-2 rounded py-1 px-1 hover:bg-muted/50 transition-colors group"
|
|
95
|
+
style={{ paddingLeft: `${indent + 4}px` }}
|
|
96
|
+
>
|
|
97
|
+
{/* tree line connector */}
|
|
98
|
+
{depth > 0 && (
|
|
99
|
+
<span className="shrink-0 w-3 h-px bg-border" aria-hidden />
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<span
|
|
103
|
+
className={`shrink-0 flex items-center justify-center rounded p-1 ${
|
|
104
|
+
isLeaf ? 'bg-muted/60' : 'bg-primary/10'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
<MenuIcon
|
|
108
|
+
name={node.icon}
|
|
109
|
+
className={`h-3.5 w-3.5 ${isLeaf ? 'text-muted-foreground' : 'text-primary'}`}
|
|
110
|
+
/>
|
|
111
|
+
</span>
|
|
112
|
+
|
|
113
|
+
<div className="flex-1 min-w-0">
|
|
114
|
+
<span className="text-sm leading-tight truncate block">
|
|
115
|
+
{node.name}
|
|
116
|
+
</span>
|
|
117
|
+
{node.url && (
|
|
118
|
+
<span className="text-xs text-muted-foreground truncate block font-mono">
|
|
119
|
+
{node.url}
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
{node.roles && node.roles.length > 0 && (
|
|
123
|
+
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
124
|
+
{node.roles.map((role) => (
|
|
125
|
+
<span
|
|
126
|
+
key={role.slug}
|
|
127
|
+
className="inline-block rounded-full bg-muted px-1.5 py-0 text-[10px] font-medium text-muted-foreground leading-4"
|
|
128
|
+
title={role.slug}
|
|
129
|
+
>
|
|
130
|
+
{role.name || role.slug}
|
|
131
|
+
</span>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{node.children.map((child) => (
|
|
139
|
+
<MenuTreeNode key={child.id} node={child} depth={depth + 1} />
|
|
140
|
+
))}
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function UserAccessSheet({
|
|
146
|
+
open,
|
|
147
|
+
onOpenChange,
|
|
148
|
+
userId,
|
|
149
|
+
userName,
|
|
150
|
+
}: UserAccessSheetProps) {
|
|
151
|
+
const t = useTranslations('core.UserPage');
|
|
152
|
+
const { request, currentLocaleCode } = useApp();
|
|
153
|
+
|
|
154
|
+
const { data: menus = [], isLoading: loadingMenus } = useQuery<UserMenu[]>({
|
|
155
|
+
queryKey: ['user-menus', userId, currentLocaleCode],
|
|
156
|
+
queryFn: async () => {
|
|
157
|
+
const res = await request<UserMenu[]>({
|
|
158
|
+
url: `/user/${userId}/menu`,
|
|
159
|
+
method: 'GET',
|
|
160
|
+
});
|
|
161
|
+
return res.data || [];
|
|
162
|
+
},
|
|
163
|
+
enabled: open && !!userId,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const { data: routes = [], isLoading: loadingRoutes } = useQuery<UserRoute[]>(
|
|
167
|
+
{
|
|
168
|
+
queryKey: ['user-routes', userId],
|
|
169
|
+
queryFn: async () => {
|
|
170
|
+
const res = await request<UserRoute[]>({
|
|
171
|
+
url: `/user/${userId}/route`,
|
|
172
|
+
method: 'GET',
|
|
173
|
+
});
|
|
174
|
+
return res.data || [];
|
|
175
|
+
},
|
|
176
|
+
enabled: open && !!userId,
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const menuTree = buildTree(menus);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
184
|
+
<SheetContent className="w-full sm:max-w-xl overflow-y-auto gap-0">
|
|
185
|
+
<SheetHeader className="px-6 pt-6 pb-4">
|
|
186
|
+
<SheetTitle className="flex items-center gap-2">
|
|
187
|
+
<Lock className="h-5 w-5 text-primary" />
|
|
188
|
+
{t('accessViewTitle')}
|
|
189
|
+
</SheetTitle>
|
|
190
|
+
<SheetDescription>
|
|
191
|
+
{userName
|
|
192
|
+
? t('accessViewDescriptionUser', { name: userName })
|
|
193
|
+
: t('accessViewDescription')}
|
|
194
|
+
</SheetDescription>
|
|
195
|
+
</SheetHeader>
|
|
196
|
+
|
|
197
|
+
<div className="w-full border-t" />
|
|
198
|
+
|
|
199
|
+
<Tabs defaultValue="menu" className="px-6 pt-4">
|
|
200
|
+
<TabsList className="w-full mb-4">
|
|
201
|
+
<TabsTrigger
|
|
202
|
+
value="menu"
|
|
203
|
+
className="flex-1 flex items-center gap-2"
|
|
204
|
+
>
|
|
205
|
+
<Menu className="h-4 w-4" />
|
|
206
|
+
{t('accessTabMenu')}
|
|
207
|
+
{!loadingMenus && (
|
|
208
|
+
<Badge variant="secondary" className="ml-1 text-xs">
|
|
209
|
+
{menus.length}
|
|
210
|
+
</Badge>
|
|
211
|
+
)}
|
|
212
|
+
</TabsTrigger>
|
|
213
|
+
<TabsTrigger
|
|
214
|
+
value="routes"
|
|
215
|
+
className="flex-1 flex items-center gap-2"
|
|
216
|
+
>
|
|
217
|
+
<Globe className="h-4 w-4" />
|
|
218
|
+
{t('accessTabRoutes')}
|
|
219
|
+
{!loadingRoutes && (
|
|
220
|
+
<Badge variant="secondary" className="ml-1 text-xs">
|
|
221
|
+
{routes.length}
|
|
222
|
+
</Badge>
|
|
223
|
+
)}
|
|
224
|
+
</TabsTrigger>
|
|
225
|
+
</TabsList>
|
|
226
|
+
|
|
227
|
+
<TabsContent value="menu" className="mt-0">
|
|
228
|
+
{loadingMenus ? (
|
|
229
|
+
<div className="flex items-center justify-center py-12">
|
|
230
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
231
|
+
<span className="ml-2 text-sm text-muted-foreground">
|
|
232
|
+
{t('loading')}
|
|
233
|
+
</span>
|
|
234
|
+
</div>
|
|
235
|
+
) : menuTree.length === 0 ? (
|
|
236
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
237
|
+
<Menu className="h-10 w-10 text-muted-foreground/40 mb-3" />
|
|
238
|
+
<p className="text-sm text-muted-foreground">
|
|
239
|
+
{t('accessNoMenus')}
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
) : (
|
|
243
|
+
<div className="rounded-lg border border-border/60 bg-card px-2 py-2">
|
|
244
|
+
{menuTree.map((node) => (
|
|
245
|
+
<MenuTreeNode key={node.id} node={node} depth={0} />
|
|
246
|
+
))}
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</TabsContent>
|
|
250
|
+
|
|
251
|
+
<TabsContent value="routes" className="space-y-2 mt-0">
|
|
252
|
+
{loadingRoutes ? (
|
|
253
|
+
<div className="flex items-center justify-center py-12">
|
|
254
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
255
|
+
<span className="ml-2 text-sm text-muted-foreground">
|
|
256
|
+
{t('loading')}
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
) : routes.length === 0 ? (
|
|
260
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
261
|
+
<Globe className="h-10 w-10 text-muted-foreground/40 mb-3" />
|
|
262
|
+
<p className="text-sm text-muted-foreground">
|
|
263
|
+
{t('accessNoRoutes')}
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
) : (
|
|
267
|
+
routes.map((route) => (
|
|
268
|
+
<div
|
|
269
|
+
key={route.id}
|
|
270
|
+
className="flex items-start gap-3 rounded-lg border border-border/60 bg-card px-4 py-2.5 hover:border-primary/40 transition-colors"
|
|
271
|
+
>
|
|
272
|
+
<span
|
|
273
|
+
className={`shrink-0 rounded border px-2 py-0.5 text-xs font-mono font-semibold ${
|
|
274
|
+
METHOD_COLORS[route.method] ??
|
|
275
|
+
'bg-muted text-muted-foreground border-border'
|
|
276
|
+
}`}
|
|
277
|
+
>
|
|
278
|
+
{route.method}
|
|
279
|
+
</span>
|
|
280
|
+
<div className="flex-1 min-w-0">
|
|
281
|
+
<p className="text-sm font-mono truncate">{route.url}</p>
|
|
282
|
+
{route.roles && route.roles.length > 0 && (
|
|
283
|
+
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
284
|
+
{route.roles.map((role) => (
|
|
285
|
+
<span
|
|
286
|
+
key={role.slug}
|
|
287
|
+
className="inline-block rounded-full bg-muted px-1.5 py-0 text-[10px] font-medium text-muted-foreground leading-4"
|
|
288
|
+
title={role.slug}
|
|
289
|
+
>
|
|
290
|
+
{role.name || role.slug}
|
|
291
|
+
</span>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
))
|
|
298
|
+
)}
|
|
299
|
+
</TabsContent>
|
|
300
|
+
</Tabs>
|
|
301
|
+
</SheetContent>
|
|
302
|
+
</Sheet>
|
|
303
|
+
);
|
|
304
|
+
}
|