@nsxbet/admin-sdk 0.5.1 → 0.6.0
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/CHECKLIST.md +40 -10
- package/README.md +337 -36
- package/dist/auth/client/gateway-token.d.ts +19 -0
- package/dist/auth/client/gateway-token.js +89 -0
- package/dist/auth/client/in-memory.d.ts +5 -1
- package/dist/auth/client/in-memory.js +75 -38
- package/dist/auth/client/index.d.ts +0 -1
- package/dist/auth/client/interface.d.ts +6 -3
- package/dist/auth/client/keycloak.d.ts +0 -1
- package/dist/auth/client/keycloak.js +6 -3
- package/dist/auth/components/UserSelector.d.ts +0 -1
- package/dist/auth/components/UserSelector.js +89 -7
- package/dist/auth/components/index.d.ts +0 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/components/AuthProvider.d.ts +0 -1
- package/dist/components/Timestamp.d.ts +7 -0
- package/dist/components/Timestamp.js +50 -0
- package/dist/hooks/useAuth.d.ts +0 -1
- package/dist/hooks/useAuth.js +1 -1
- package/dist/hooks/useFetch.d.ts +0 -1
- package/dist/hooks/useI18n.d.ts +0 -1
- package/dist/hooks/usePlatformAPI.d.ts +0 -1
- package/dist/hooks/useTelemetry.d.ts +0 -1
- package/dist/hooks/useTimestamp.d.ts +8 -0
- package/dist/hooks/useTimestamp.js +122 -0
- package/dist/i18n/config.d.ts +20 -2
- package/dist/i18n/config.js +48 -0
- package/dist/i18n/index.d.ts +2 -3
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en-US.json +95 -18
- package/dist/i18n/locales/es.json +95 -18
- package/dist/i18n/locales/pt-BR.json +95 -18
- package/dist/i18n/locales/ro.json +95 -18
- package/dist/index.d.ts +11 -7
- package/dist/index.js +5 -1
- package/dist/registry/AdminShellRegistry.d.ts +1 -2
- package/dist/registry/cache/cached-catalog.d.ts +11 -0
- package/dist/registry/cache/cached-catalog.js +42 -0
- package/dist/registry/cache/catalog-cache.d.ts +10 -0
- package/dist/registry/cache/catalog-cache.js +58 -0
- package/dist/registry/cache/index.d.ts +5 -0
- package/dist/registry/cache/index.js +3 -0
- package/dist/registry/cache/types.d.ts +20 -0
- package/dist/registry/cache/types.js +3 -0
- package/dist/registry/client/http.d.ts +0 -1
- package/dist/registry/client/http.js +13 -0
- package/dist/registry/client/in-memory.d.ts +0 -1
- package/dist/registry/client/in-memory.js +117 -12
- package/dist/registry/client/index.d.ts +0 -1
- package/dist/registry/client/interface.d.ts +21 -6
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +4 -0
- package/dist/registry/types/index.d.ts +2 -3
- package/dist/registry/types/manifest.d.ts +20 -24
- package/dist/registry/types/manifest.js +17 -18
- package/dist/registry/types/module.d.ts +43 -14
- package/dist/registry/useRegistryPolling.d.ts +15 -0
- package/dist/registry/useRegistryPolling.js +66 -0
- package/dist/router/DynamicModule.d.ts +6 -22
- package/dist/router/DynamicModule.js +25 -48
- package/dist/router/ModuleErrorBoundary.d.ts +39 -0
- package/dist/router/ModuleErrorBoundary.js +101 -0
- package/dist/router/index.d.ts +1 -1
- package/dist/router/url-allowlist.d.ts +22 -0
- package/dist/router/url-allowlist.js +65 -0
- package/dist/shell/AdminShell.d.ts +0 -1
- package/dist/shell/AdminShell.js +178 -43
- package/dist/shell/BackofficeShell.d.ts +0 -1
- package/dist/shell/BackofficeShell.js +59 -25
- package/dist/shell/components/CommandPalette.d.ts +0 -1
- package/dist/shell/components/CommandPalette.js +26 -50
- package/dist/shell/components/DevtoolsPanel.d.ts +11 -0
- package/dist/shell/components/DevtoolsPanel.js +145 -0
- package/dist/shell/components/HomePage.d.ts +0 -1
- package/dist/shell/components/HomePage.js +9 -4
- package/dist/shell/components/LeftNav.d.ts +0 -1
- package/dist/shell/components/LeftNav.js +91 -93
- package/dist/shell/components/MainContent.d.ts +3 -2
- package/dist/shell/components/MainContent.js +8 -23
- package/dist/shell/components/ModuleOverview.d.ts +0 -1
- package/dist/shell/components/ModuleOverview.js +4 -20
- package/dist/shell/components/ProfilePage.d.ts +0 -1
- package/dist/shell/components/ProfilePage.js +1 -1
- package/dist/shell/components/RegistryPage.d.ts +0 -1
- package/dist/shell/components/RegistryPage.js +154 -64
- package/dist/shell/components/RegistryStatusBanner.d.ts +6 -0
- package/dist/shell/components/RegistryStatusBanner.js +31 -0
- package/dist/shell/components/RegistryUnavailable.d.ts +4 -0
- package/dist/shell/components/RegistryUnavailable.js +7 -0
- package/dist/shell/components/SettingsPage.d.ts +0 -1
- package/dist/shell/components/StackedPanel.d.ts +15 -0
- package/dist/shell/components/StackedPanel.js +45 -0
- package/dist/shell/components/TopBar.d.ts +4 -2
- package/dist/shell/components/TopBar.js +9 -3
- package/dist/shell/components/UpdateBanner.d.ts +5 -0
- package/dist/shell/components/UpdateBanner.js +8 -0
- package/dist/shell/components/index.d.ts +4 -1
- package/dist/shell/components/index.js +2 -0
- package/dist/shell/components/theme-provider.d.ts +0 -1
- package/dist/shell/components/theme-provider.js +8 -5
- package/dist/shell/hooks/useCspViolations.d.ts +12 -0
- package/dist/shell/hooks/useCspViolations.js +34 -0
- package/dist/shell/index.d.ts +1 -2
- package/dist/shell/polling-config.d.ts +10 -0
- package/dist/shell/polling-config.js +26 -0
- package/dist/shell/search/fuzzy.d.ts +0 -1
- package/dist/shell/search/index.d.ts +0 -1
- package/dist/shell/telemetry.d.ts +0 -1
- package/dist/shell/types.d.ts +34 -18
- package/dist/tailwind/index.d.ts +0 -1
- package/dist/types/keycloak.d.ts +0 -1
- package/dist/types/platform.d.ts +12 -1
- package/dist/vite/AdminShellSharedDeps.d.ts +64 -0
- package/dist/vite/AdminShellSharedDeps.js +215 -0
- package/dist/vite/config.d.ts +2 -2
- package/dist/vite/config.js +5 -7
- package/dist/vite/i18n-plugin.d.ts +13 -0
- package/dist/vite/i18n-plugin.js +81 -0
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +2 -0
- package/dist/vite/plugins.d.ts +0 -1
- package/package.json +6 -2
- package/dist/auth/client/in-memory.d.ts.map +0 -1
- package/dist/auth/client/index.d.ts.map +0 -1
- package/dist/auth/client/interface.d.ts.map +0 -1
- package/dist/auth/client/keycloak.d.ts.map +0 -1
- package/dist/auth/components/UserSelector.d.ts.map +0 -1
- package/dist/auth/components/index.d.ts.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/components/AuthProvider.d.ts.map +0 -1
- package/dist/hooks/useAuth.d.ts.map +0 -1
- package/dist/hooks/useFetch.d.ts.map +0 -1
- package/dist/hooks/useI18n.d.ts.map +0 -1
- package/dist/hooks/usePlatformAPI.d.ts.map +0 -1
- package/dist/hooks/useTelemetry.d.ts.map +0 -1
- package/dist/i18n/config.d.ts.map +0 -1
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/registry/AdminShellRegistry.d.ts.map +0 -1
- package/dist/registry/client/http.d.ts.map +0 -1
- package/dist/registry/client/in-memory.d.ts.map +0 -1
- package/dist/registry/client/index.d.ts.map +0 -1
- package/dist/registry/client/interface.d.ts.map +0 -1
- package/dist/registry/index.d.ts.map +0 -1
- package/dist/registry/types/index.d.ts.map +0 -1
- package/dist/registry/types/manifest.d.ts.map +0 -1
- package/dist/registry/types/module.d.ts.map +0 -1
- package/dist/router/DynamicModule.d.ts.map +0 -1
- package/dist/router/index.d.ts.map +0 -1
- package/dist/shell/AdminShell.d.ts.map +0 -1
- package/dist/shell/BackofficeShell.d.ts.map +0 -1
- package/dist/shell/components/CommandPalette.d.ts.map +0 -1
- package/dist/shell/components/HomePage.d.ts.map +0 -1
- package/dist/shell/components/LeftNav.d.ts.map +0 -1
- package/dist/shell/components/MainContent.d.ts.map +0 -1
- package/dist/shell/components/ModuleOverview.d.ts.map +0 -1
- package/dist/shell/components/ProfilePage.d.ts.map +0 -1
- package/dist/shell/components/RegistryPage.d.ts.map +0 -1
- package/dist/shell/components/SettingsPage.d.ts.map +0 -1
- package/dist/shell/components/TopBar.d.ts.map +0 -1
- package/dist/shell/components/index.d.ts.map +0 -1
- package/dist/shell/components/theme-provider.d.ts.map +0 -1
- package/dist/shell/index.d.ts.map +0 -1
- package/dist/shell/search/fuzzy.d.ts.map +0 -1
- package/dist/shell/search/index.d.ts.map +0 -1
- package/dist/shell/telemetry.d.ts.map +0 -1
- package/dist/shell/types.d.ts.map +0 -1
- package/dist/tailwind/index.d.ts.map +0 -1
- package/dist/types/keycloak.d.ts.map +0 -1
- package/dist/types/platform.d.ts.map +0 -1
- package/dist/vite/config.d.ts.map +0 -1
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/plugins.d.ts.map +0 -1
|
@@ -4,7 +4,9 @@ import { useNavigate, useLocation } from "react-router-dom";
|
|
|
4
4
|
import { useAuth } from "../../hooks/useAuth";
|
|
5
5
|
import { useAuthContext } from "../../components/AuthProvider";
|
|
6
6
|
import { useI18n } from "../../hooks/useI18n";
|
|
7
|
+
import { resolveLocalizedString } from "../../i18n";
|
|
7
8
|
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarHeader, SidebarFooter, SidebarTrigger, useSidebar, Collapsible, CollapsibleContent, CollapsibleTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Icon, ChevronRight, ChevronUp, ChevronDown, Lock, Pin, } from "@nsxbet/admin-ui";
|
|
9
|
+
import { StackedPanel } from "./StackedPanel";
|
|
8
10
|
export function LeftNav({ modules }) {
|
|
9
11
|
const navigate = useNavigate();
|
|
10
12
|
const location = useLocation();
|
|
@@ -27,13 +29,13 @@ export function LeftNav({ modules }) {
|
|
|
27
29
|
// Ignore localStorage errors
|
|
28
30
|
}
|
|
29
31
|
}, []);
|
|
30
|
-
// Load pinned commands from localStorage
|
|
31
32
|
useEffect(() => {
|
|
32
33
|
const loadPinnedCommands = () => {
|
|
33
34
|
try {
|
|
34
35
|
const pinned = localStorage.getItem("adminPlatform.pinnedCommands");
|
|
35
36
|
if (pinned) {
|
|
36
|
-
|
|
37
|
+
const parsed = JSON.parse(pinned);
|
|
38
|
+
setPinnedCommands(parsed.filter((p) => p.moduleId && p.commandId && p.route));
|
|
37
39
|
}
|
|
38
40
|
else {
|
|
39
41
|
setPinnedCommands([]);
|
|
@@ -63,16 +65,13 @@ export function LeftNav({ modules }) {
|
|
|
63
65
|
return () => window.removeEventListener("storage", handleStorageChange);
|
|
64
66
|
}, []);
|
|
65
67
|
const handleModuleClick = (module) => {
|
|
66
|
-
// Navigate to the shell-rendered overview page
|
|
67
68
|
navigate(`/_modules/${module.id}`);
|
|
68
69
|
};
|
|
69
|
-
// Helper to check if user has permission to view a module
|
|
70
70
|
const checkModulePermission = (module) => {
|
|
71
71
|
if (module.permissions.view.length === 0)
|
|
72
72
|
return true;
|
|
73
73
|
return module.permissions.view.some((perm) => auth.hasPermission(perm));
|
|
74
74
|
};
|
|
75
|
-
// Group modules by category (show all active modules, including those without permission)
|
|
76
75
|
const modulesByCategory = useMemo(() => {
|
|
77
76
|
const grouped = modules
|
|
78
77
|
.filter((m) => m.status === "active")
|
|
@@ -88,93 +87,77 @@ export function LeftNav({ modules }) {
|
|
|
88
87
|
acc[category].push(module);
|
|
89
88
|
return acc;
|
|
90
89
|
}, {});
|
|
91
|
-
// Sort modules within each category by navOrder then title
|
|
92
90
|
Object.keys(grouped).forEach((category) => {
|
|
93
91
|
grouped[category].sort((a, b) => {
|
|
94
|
-
if (a.
|
|
95
|
-
return a.
|
|
92
|
+
if (a.navigationOrder !== undefined && b.navigationOrder !== undefined) {
|
|
93
|
+
return a.navigationOrder - b.navigationOrder;
|
|
96
94
|
}
|
|
97
|
-
if (a.
|
|
95
|
+
if (a.navigationOrder !== undefined)
|
|
98
96
|
return -1;
|
|
99
|
-
if (b.
|
|
97
|
+
if (b.navigationOrder !== undefined)
|
|
100
98
|
return 1;
|
|
101
|
-
return a.title.localeCompare(b.title);
|
|
99
|
+
return resolveLocalizedString(a.title, i18n.language).localeCompare(resolveLocalizedString(b.title, i18n.language));
|
|
102
100
|
});
|
|
103
101
|
});
|
|
104
102
|
return grouped;
|
|
105
|
-
}, [modules, auth]);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
}, [modules, auth, i18n.language]);
|
|
104
|
+
const categories = Object.keys(modulesByCategory).sort((a, b) => {
|
|
105
|
+
const minOrder = (cat) => {
|
|
106
|
+
const mods = modulesByCategory[cat];
|
|
107
|
+
return Math.min(...mods.map((m) => m.navigationOrder ?? Number.MAX_SAFE_INTEGER));
|
|
108
|
+
};
|
|
109
|
+
return minOrder(a) - minOrder(b);
|
|
110
|
+
});
|
|
109
111
|
const noPermissionTooltip = t('errors.accessDeniedDescription');
|
|
110
|
-
// Check if a route is active
|
|
111
112
|
const isRouteActive = (route) => {
|
|
112
113
|
return location.pathname === route || location.pathname.startsWith(route + "/");
|
|
113
114
|
};
|
|
114
|
-
// Helper to translate module/command titles
|
|
115
|
-
// titleKey format: "namespace:key" (e.g., "tasks:module.title")
|
|
116
|
-
const translateTitle = useCallback((title, titleKey) => {
|
|
117
|
-
if (titleKey && titleKey.includes(':')) {
|
|
118
|
-
// Parse "namespace:key" format
|
|
119
|
-
const [ns, key] = titleKey.split(':');
|
|
120
|
-
// Use i18n.t with explicit namespace option
|
|
121
|
-
const translated = i18n.t(key, { ns, defaultValue: title });
|
|
122
|
-
return translated;
|
|
123
|
-
}
|
|
124
|
-
if (titleKey) {
|
|
125
|
-
// Fallback: try using the titleKey directly
|
|
126
|
-
const translated = t(titleKey, { defaultValue: title });
|
|
127
|
-
return translated;
|
|
128
|
-
}
|
|
129
|
-
return title;
|
|
130
|
-
}, [i18n, t]);
|
|
131
|
-
// Check if a command is pinned
|
|
132
115
|
const isCommandPinned = useCallback((moduleId, commandId) => {
|
|
133
116
|
return pinnedCommands.some((p) => p.moduleId === moduleId && p.commandId === commandId);
|
|
134
117
|
}, [pinnedCommands]);
|
|
135
|
-
|
|
118
|
+
const getPinnedDisplayTitles = useCallback((pinned) => {
|
|
119
|
+
const module = modules.find((m) => m.id === pinned.moduleId);
|
|
120
|
+
const command = module?.commands?.find((c) => c.id === pinned.commandId);
|
|
121
|
+
const moduleTitle = module
|
|
122
|
+
? resolveLocalizedString(module.title, i18n.language)
|
|
123
|
+
: pinned.moduleId;
|
|
124
|
+
const commandTitle = command
|
|
125
|
+
? resolveLocalizedString(command.title, i18n.language)
|
|
126
|
+
: pinned.commandId;
|
|
127
|
+
return { moduleTitle, commandTitle };
|
|
128
|
+
}, [modules, i18n.language]);
|
|
136
129
|
const togglePin = useCallback((module, command) => {
|
|
137
130
|
const isPinned = isCommandPinned(module.id, command.id);
|
|
138
131
|
let updated;
|
|
139
132
|
if (isPinned) {
|
|
140
|
-
// Unpin
|
|
141
133
|
updated = pinnedCommands.filter((p) => !(p.moduleId === module.id && p.commandId === command.id));
|
|
142
134
|
}
|
|
143
135
|
else {
|
|
144
|
-
// Pin
|
|
145
136
|
const pinnedCommand = {
|
|
146
137
|
moduleId: module.id,
|
|
147
|
-
moduleTitle: module.title,
|
|
148
|
-
moduleTitleKey: module.titleKey,
|
|
149
|
-
moduleIcon: module.icon,
|
|
150
138
|
commandId: command.id,
|
|
151
|
-
commandTitle: command.title,
|
|
152
|
-
commandTitleKey: command.titleKey,
|
|
153
|
-
commandIcon: command.icon,
|
|
154
139
|
route: command.route,
|
|
140
|
+
moduleIcon: module.icon,
|
|
141
|
+
commandIcon: command.icon,
|
|
155
142
|
};
|
|
156
143
|
updated = [...pinnedCommands, pinnedCommand];
|
|
157
144
|
}
|
|
158
145
|
setPinnedCommands(updated);
|
|
159
146
|
localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
|
|
160
|
-
// Notify other components by dispatching a storage event
|
|
161
147
|
window.dispatchEvent(new StorageEvent("storage", {
|
|
162
148
|
key: "adminPlatform.pinnedCommands",
|
|
163
149
|
newValue: JSON.stringify(updated),
|
|
164
150
|
}));
|
|
165
|
-
}, [pinnedCommands, isCommandPinned]);
|
|
166
|
-
// Unpin a command by moduleId and commandId
|
|
151
|
+
}, [pinnedCommands, isCommandPinned, modules]);
|
|
167
152
|
const unpinCommand = useCallback((moduleId, commandId) => {
|
|
168
153
|
const updated = pinnedCommands.filter((p) => !(p.moduleId === moduleId && p.commandId === commandId));
|
|
169
154
|
setPinnedCommands(updated);
|
|
170
155
|
localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
|
|
171
|
-
// Notify other components by dispatching a storage event
|
|
172
156
|
window.dispatchEvent(new StorageEvent("storage", {
|
|
173
157
|
key: "adminPlatform.pinnedCommands",
|
|
174
158
|
newValue: JSON.stringify(updated),
|
|
175
159
|
}));
|
|
176
160
|
}, [pinnedCommands]);
|
|
177
|
-
// Move a pinned item up in the list
|
|
178
161
|
const movePinnedUp = useCallback((index) => {
|
|
179
162
|
if (index <= 0)
|
|
180
163
|
return;
|
|
@@ -187,7 +170,6 @@ export function LeftNav({ modules }) {
|
|
|
187
170
|
newValue: JSON.stringify(updated),
|
|
188
171
|
}));
|
|
189
172
|
}, [pinnedCommands]);
|
|
190
|
-
// Move a pinned item down in the list
|
|
191
173
|
const movePinnedDown = useCallback((index) => {
|
|
192
174
|
if (index >= pinnedCommands.length - 1)
|
|
193
175
|
return;
|
|
@@ -200,48 +182,64 @@ export function LeftNav({ modules }) {
|
|
|
200
182
|
newValue: JSON.stringify(updated),
|
|
201
183
|
}));
|
|
202
184
|
}, [pinnedCommands]);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
185
|
+
// URL-driven stacked panel detection
|
|
186
|
+
const activeStackedModule = useMemo(() => {
|
|
187
|
+
return modules.find((m) => m.navigation?.style === "stacked" &&
|
|
188
|
+
m.status === "active" &&
|
|
189
|
+
(location.pathname === m.routeBase ||
|
|
190
|
+
location.pathname.startsWith(m.routeBase + "/"))) ?? null;
|
|
191
|
+
}, [modules, location.pathname]);
|
|
192
|
+
const isStacked = activeStackedModule !== null;
|
|
193
|
+
const isStackedModule = (module) => module.navigation?.style === "stacked";
|
|
194
|
+
const userFooter = (_jsx(SidebarFooter, { className: `border-t border-sidebar-border ${isCollapsed ? "items-center" : ""}`, children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", tooltip: user?.displayName || "User", "data-testid": "user-menu-trigger", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.displayName || "User" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email || "" })] }), _jsx(ChevronUp, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { side: isCollapsed ? "right" : "top", align: isCollapsed ? "end" : "start", className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", children: [_jsxs(DropdownMenuItem, { onClick: () => navigate("/_profile"), className: isRouteActive("/_profile") ? "bg-accent" : "", "data-testid": "nav-profile", children: [_jsx(Icon, { name: "user", className: "h-4 w-4 mr-2" }), t('navigation.profile')] }), _jsxs(DropdownMenuItem, { onClick: () => navigate("/_settings"), className: isRouteActive("/_settings") ? "bg-accent" : "", "data-testid": "nav-settings", children: [_jsx(Icon, { name: "settings", className: "h-4 w-4 mr-2" }), t('navigation.settings')] }), auth.hasPermission('admin.platform.view') && (_jsxs(DropdownMenuItem, { onClick: () => navigate("/_registry"), className: isRouteActive("/_registry") ? "bg-accent" : "", "data-testid": "nav-registry", children: [_jsx(Icon, { name: "package", className: "h-4 w-4 mr-2" }), t('navigation.registry')] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => logout(), "data-testid": "nav-logout", children: [_jsx(Icon, { name: "log-out", className: "h-4 w-4 mr-2" }), t('navigation.logout')] })] })] }) }) }) }));
|
|
195
|
+
return (_jsx(Sidebar, { collapsible: "icon", "data-testid": "left-nav", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsx(SidebarTrigger, { "data-testid": "sidebar-toggle" }) }) }) }), _jsxs("div", { className: "flex flex-1 min-h-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none", style: {
|
|
196
|
+
width: "200%",
|
|
197
|
+
transform: isStacked && !isCollapsed ? "translateX(-50%)" : "translateX(0)",
|
|
198
|
+
}, children: [_jsx("div", { className: "w-1/2 flex flex-col min-h-0", children: _jsxs(SidebarContent, { className: "flex-1 min-h-0", children: [showPinned && pinnedCommands.length > 0 && (_jsxs(SidebarGroup, { "data-testid": "pinned-section", children: [_jsxs(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: [_jsx(Pin, { className: "mr-2 h-4 w-4" }), !isCollapsed && t('navigation.pinned')] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: pinnedCommands.map((pinned, index) => {
|
|
199
|
+
const iconName = pinned.commandIcon || pinned.moduleIcon || "file-text";
|
|
200
|
+
const isFirst = index === 0;
|
|
201
|
+
const isLast = index === pinnedCommands.length - 1;
|
|
202
|
+
return (_jsx(SidebarMenuItem, { className: "group/pinned", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsxs(SidebarMenuButton, { onClick: () => navigate(pinned.route), isActive: isRouteActive(pinned.route), tooltip: `${getPinnedDisplayTitles(pinned).moduleTitle} / ${getPinnedDisplayTitles(pinned).commandTitle}`, "data-testid": `pinned-${pinned.commandId}`, className: "flex-1 min-w-0", children: [_jsx(Icon, { name: iconName, className: "h-4 w-4 shrink-0" }), _jsxs("span", { className: "truncate", children: [getPinnedDisplayTitles(pinned).moduleTitle, " / ", getPinnedDisplayTitles(pinned).commandTitle] })] }), !isCollapsed && (_jsxs("div", { className: "flex items-center gap-0.5 shrink-0 pr-2 opacity-0 group-hover/pinned:opacity-100 transition-opacity", children: [!isFirst && (_jsx("button", { onClick: (e) => {
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
movePinnedUp(index);
|
|
206
|
+
}, className: "hover:text-primary p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Move up", "data-testid": `move-up-${pinned.commandId}`, children: _jsx(ChevronUp, { className: "h-3.5 w-3.5" }) })), !isLast && (_jsx("button", { onClick: (e) => {
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
movePinnedDown(index);
|
|
210
|
+
}, className: "hover:text-primary p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Move down", "data-testid": `move-down-${pinned.commandId}`, children: _jsx(ChevronDown, { className: "h-3.5 w-3.5" }) })), _jsx("button", { onClick: (e) => {
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
unpinCommand(pinned.moduleId, pinned.commandId);
|
|
214
|
+
}, className: "hover:text-destructive p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Unpin", "data-testid": `unpin-${pinned.commandId}`, children: _jsx(Pin, { className: "h-3.5 w-3.5 fill-current" }) })] }))] }) }, `${pinned.moduleId}:${pinned.commandId}`));
|
|
215
|
+
}) }) })] })), categories.map((category) => (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { "data-testid": `category-${category}`, className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: category }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { "data-testid": `category-modules-${category}`, children: modulesByCategory[category].map((module) => {
|
|
216
|
+
const hasCommands = module.commands && module.commands.length > 0;
|
|
217
|
+
const isDisabled = !module.hasViewPermission;
|
|
218
|
+
if (!hasCommands) {
|
|
219
|
+
const moduleTitle = resolveLocalizedString(module.title, i18n.language);
|
|
220
|
+
return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => !isDisabled && handleModuleClick(module), className: isDisabled ? "opacity-50 cursor-not-allowed" : "", tooltip: isDisabled ? noPermissionTooltip : moduleTitle, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: moduleTitle }), isDisabled && _jsx(Lock, { className: "ml-auto h-3 w-3 text-muted-foreground" })] }) }, module.id));
|
|
221
|
+
}
|
|
222
|
+
if (isDisabled) {
|
|
223
|
+
const disabledModuleTitle = resolveLocalizedString(module.title, i18n.language);
|
|
224
|
+
return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { className: "opacity-50 cursor-not-allowed", tooltip: noPermissionTooltip, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: disabledModuleTitle }), _jsx(Lock, { className: "ml-auto h-3 w-3 text-muted-foreground" })] }) }, module.id));
|
|
225
|
+
}
|
|
226
|
+
// Stacked module: single button with chevron-right
|
|
227
|
+
if (isStackedModule(module)) {
|
|
228
|
+
const stackedModuleTitle = resolveLocalizedString(module.title, i18n.language);
|
|
229
|
+
return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => navigate(module.routeBase), isActive: isRouteActive(module.routeBase), tooltip: stackedModuleTitle, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: stackedModuleTitle }), _jsx(ChevronRight, { className: "ml-auto h-4 w-4" })] }) }, module.id));
|
|
230
|
+
}
|
|
231
|
+
// Collapsible module (default)
|
|
232
|
+
const collapsibleModuleTitle = resolveLocalizedString(module.title, i18n.language);
|
|
233
|
+
return (_jsx(Collapsible, { asChild: true, className: "group/collapsible", children: _jsxs(SidebarMenuItem, { children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { tooltip: collapsibleModuleTitle, "data-testid": `module-${module.id}-trigger`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: collapsibleModuleTitle }), _jsx(ChevronRight, { className: "ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-90" })] }) }), _jsx(CollapsibleContent, { children: _jsxs(SidebarMenuSub, { children: [_jsx(SidebarMenuSubItem, { children: _jsx(SidebarMenuSubButton, { onClick: () => handleModuleClick(module), isActive: location.pathname === `/_modules/${module.id}`, "data-testid": `module-${module.id}`, children: t('navigation.overview') }) }), module.commands?.map((command) => {
|
|
234
|
+
const isPinned = isCommandPinned(module.id, command.id);
|
|
235
|
+
const commandTitle = resolveLocalizedString(command.title, i18n.language);
|
|
236
|
+
return (_jsx(SidebarMenuSubItem, { className: "group/command", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsx(SidebarMenuSubButton, { onClick: () => navigate(command.route), isActive: isRouteActive(command.route), "data-testid": `command-${command.id}`, className: "flex-1 min-w-0", children: _jsx("span", { className: "truncate", children: commandTitle }) }), showPinned && (_jsx("button", { onClick: (e) => {
|
|
237
|
+
e.stopPropagation();
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
togglePin(module, command);
|
|
240
|
+
}, className: `shrink-0 p-0.5 rounded mr-2 transition-opacity ${isPinned
|
|
241
|
+
? "text-primary hover:text-destructive hover:bg-sidebar-accent opacity-100"
|
|
242
|
+
: "text-muted-foreground hover:text-primary hover:bg-sidebar-accent opacity-0 group-hover/command:opacity-100"}`, title: isPinned ? "Unpin" : "Pin", "data-testid": `pin-toggle-${command.id}`, children: _jsx(Pin, { className: `h-3.5 w-3.5 ${isPinned ? "fill-current" : ""}` }) }))] }) }, command.id));
|
|
243
|
+
})] }) })] }) }, module.id));
|
|
244
|
+
}) }) })] }, category))), auth.hasPermission('admin.platform.view') && (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: t('navigation.platform') }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => navigate("/_registry"), isActive: isRouteActive("/_registry"), tooltip: t('navigation.registry'), "data-testid": "nav-registry-sidebar", children: [_jsx(Icon, { name: "package", className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: t('navigation.registry') })] }) }) }) })] }))] }) }), _jsx("div", { className: "w-1/2 flex flex-col min-h-0", children: activeStackedModule ? (_jsx(StackedPanel, { module: activeStackedModule, showPinned: showPinned, isCommandPinned: isCommandPinned, togglePin: togglePin })) : null })] }), userFooter] }) }));
|
|
247
245
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { Module } from "../types";
|
|
3
|
+
import type { Breadcrumb } from "../../types/platform";
|
|
3
4
|
interface MainContentProps {
|
|
4
5
|
modules: Module[];
|
|
5
6
|
children?: ReactNode;
|
|
7
|
+
moduleBreadcrumbs?: Breadcrumb[] | null;
|
|
6
8
|
}
|
|
7
|
-
export declare function MainContent({ modules, children }: MainContentProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function MainContent({ modules, children, moduleBreadcrumbs }: MainContentProps): import("react/jsx-runtime").JSX.Element;
|
|
8
10
|
export {};
|
|
9
|
-
//# sourceMappingURL=MainContent.d.ts.map
|
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useState, useEffect
|
|
2
|
+
import { Fragment, useState, useEffect } from "react";
|
|
3
3
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
4
4
|
import { useI18n } from "../../hooks/useI18n";
|
|
5
|
-
|
|
5
|
+
import { resolveLocalizedString } from "../../i18n";
|
|
6
|
+
export function MainContent({ modules, children, moduleBreadcrumbs }) {
|
|
6
7
|
const location = useLocation();
|
|
7
8
|
const navigate = useNavigate();
|
|
8
9
|
const { t, i18n } = useI18n();
|
|
9
10
|
const [breadcrumbs, setBreadcrumbs] = useState([]);
|
|
10
|
-
// Helper to translate module/command titles
|
|
11
|
-
// titleKey format: "namespace:key" (e.g., "tasks:module.title")
|
|
12
|
-
const translateTitle = useCallback((title, titleKey) => {
|
|
13
|
-
if (titleKey && titleKey.includes(':')) {
|
|
14
|
-
// Parse "namespace:key" format
|
|
15
|
-
const [ns, key] = titleKey.split(':');
|
|
16
|
-
// Use i18n.t with explicit namespace option
|
|
17
|
-
const translated = i18n.t(key, { ns, defaultValue: title });
|
|
18
|
-
return translated;
|
|
19
|
-
}
|
|
20
|
-
if (titleKey) {
|
|
21
|
-
// Fallback: try using the titleKey directly
|
|
22
|
-
const translated = t(titleKey, { defaultValue: title });
|
|
23
|
-
return translated;
|
|
24
|
-
}
|
|
25
|
-
return title;
|
|
26
|
-
}, [i18n, t]);
|
|
27
11
|
// Auto-detect module and update breadcrumbs based on current route
|
|
28
12
|
useEffect(() => {
|
|
29
13
|
const currentPath = location.pathname;
|
|
@@ -55,10 +39,10 @@ export function MainContent({ modules, children }) {
|
|
|
55
39
|
const moduleHref = module.commands && module.commands.length > 0
|
|
56
40
|
? module.commands[0].route
|
|
57
41
|
: module.routeBase;
|
|
58
|
-
crumbs.push({ label:
|
|
42
|
+
crumbs.push({ label: resolveLocalizedString(module.title, i18n.language), href: moduleHref });
|
|
59
43
|
if (command) {
|
|
60
44
|
// Add command as breadcrumb (with translation)
|
|
61
|
-
crumbs.push({ label:
|
|
45
|
+
crumbs.push({ label: resolveLocalizedString(command.title, i18n.language) });
|
|
62
46
|
}
|
|
63
47
|
else {
|
|
64
48
|
// Add sub-paths if any (for non-command routes)
|
|
@@ -83,6 +67,7 @@ export function MainContent({ modules, children }) {
|
|
|
83
67
|
// No module matched, show home
|
|
84
68
|
setBreadcrumbs([{ label: t('breadcrumbs.home') }]);
|
|
85
69
|
}
|
|
86
|
-
}, [location.pathname, modules,
|
|
87
|
-
|
|
70
|
+
}, [location.pathname, modules, i18n.language]);
|
|
71
|
+
const displayBreadcrumbs = moduleBreadcrumbs ?? breadcrumbs;
|
|
72
|
+
return (_jsxs("main", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx("div", { "data-testid": "breadcrumbs", className: "flex h-10 items-center justify-between border-b px-4 text-sm text-muted-foreground", children: _jsx("div", { className: "flex items-center gap-2", children: displayBreadcrumbs.map((crumb, index) => (_jsxs(Fragment, { children: [index > 0 && _jsx("span", { children: "/" }), crumb.href ? (_jsx("button", { onClick: () => navigate(crumb.href), className: "transition-colors hover:text-foreground", children: crumb.label })) : (_jsx("span", { className: "text-foreground", children: crumb.label }))] }, index))) }) }), _jsx("div", { className: "flex-1 overflow-auto p-4", "data-testid": "module-container", children: children })] }));
|
|
88
73
|
}
|
|
@@ -2,27 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
3
3
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Badge, Icon, } from "@nsxbet/admin-ui";
|
|
4
4
|
import { useI18n } from "../../hooks/useI18n";
|
|
5
|
+
import { resolveLocalizedString } from "../../i18n";
|
|
5
6
|
export function ModuleOverview({ modules }) {
|
|
6
7
|
const location = useLocation();
|
|
7
8
|
const navigate = useNavigate();
|
|
8
9
|
const { t, i18n } = useI18n();
|
|
9
|
-
// Helper to translate titles
|
|
10
|
-
// titleKey format: "namespace:key" (e.g., "tasks:module.title")
|
|
11
|
-
const translateTitle = (title, titleKey) => {
|
|
12
|
-
if (titleKey && titleKey.includes(':')) {
|
|
13
|
-
// Parse "namespace:key" format
|
|
14
|
-
const [ns, key] = titleKey.split(':');
|
|
15
|
-
// Use i18n.t with explicit namespace option
|
|
16
|
-
const translated = i18n.t(key, { ns, defaultValue: title });
|
|
17
|
-
return translated;
|
|
18
|
-
}
|
|
19
|
-
if (titleKey) {
|
|
20
|
-
// Fallback: try using the titleKey directly
|
|
21
|
-
const translated = t(titleKey, { defaultValue: title });
|
|
22
|
-
return translated;
|
|
23
|
-
}
|
|
24
|
-
return title;
|
|
25
|
-
};
|
|
26
10
|
// Extract moduleId from path: /_modules/xxx -> xxx
|
|
27
11
|
const moduleId = location.pathname.replace(/^\/_modules\//, "");
|
|
28
12
|
const module = modules.find((m) => m.id === moduleId);
|
|
@@ -34,7 +18,7 @@ export function ModuleOverview({ modules }) {
|
|
|
34
18
|
: module.status === "deprecated"
|
|
35
19
|
? "warning"
|
|
36
20
|
: "secondary";
|
|
37
|
-
const moduleTitle =
|
|
38
|
-
const moduleDescription =
|
|
39
|
-
return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0 p-3 rounded-lg bg-muted", children: _jsx(Icon, { name: module.icon || "package", className: "h-8 w-8" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-3 mb-1", children: [_jsx("h1", { className: "text-3xl font-bold", children: moduleTitle }), _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status })] }), moduleDescription && (_jsx("p", { className: "text-muted-foreground", children: moduleDescription }))] })] }), module.commands && module.commands.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "zap", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.actions') })] }), _jsx(CardDescription, { children: t('moduleOverview.actionsDescription') })] }), _jsx(CardContent, { children: _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: module.commands.map((command) => (_jsxs("button", { onClick: () => navigate(command.route), className: "flex items-center gap-3 rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent hover:border-accent", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-md bg-muted", children: _jsx(Icon, { name: command.icon || "file-text", className: "h-5 w-5" }) }), _jsx("span", { className: "font-medium", children:
|
|
21
|
+
const moduleTitle = resolveLocalizedString(module.title, i18n.language);
|
|
22
|
+
const moduleDescription = resolveLocalizedString(module.description, i18n.language);
|
|
23
|
+
return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0 p-3 rounded-lg bg-muted", children: _jsx(Icon, { name: module.icon || "package", className: "h-8 w-8" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-3 mb-1", children: [_jsx("h1", { className: "text-3xl font-bold", children: moduleTitle }), _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status })] }), moduleDescription && (_jsx("p", { className: "text-muted-foreground", children: moduleDescription }))] })] }), module.commands && module.commands.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "zap", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.actions') })] }), _jsx(CardDescription, { children: t('moduleOverview.actionsDescription') })] }), _jsx(CardContent, { children: _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: module.commands.map((command) => (_jsxs("button", { onClick: () => navigate(command.route), className: "flex items-center gap-3 rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent hover:border-accent", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-md bg-muted", children: _jsx(Icon, { name: command.icon || "file-text", className: "h-5 w-5" }) }), _jsx("span", { className: "font-medium", children: resolveLocalizedString(command.title, i18n.language) })] }, command.id))) }) })] })), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "info", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.information') })] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "folder", className: "h-4 w-4" }), t('moduleOverview.category')] }), _jsx("dd", { className: "text-sm font-medium", children: module.category })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "activity", className: "h-4 w-4" }), t('moduleOverview.status')] }), _jsx("dd", { children: _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status }) })] }), module.owners?.team && (_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "users", className: "h-4 w-4" }), t('moduleOverview.owner')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.team })] })), module.owners?.supportChannel && (_jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "message-circle", className: "h-4 w-4" }), t('moduleOverview.support')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.supportChannel })] }))] }) })] })] }));
|
|
40
24
|
}
|
|
@@ -26,5 +26,5 @@ export function ProfilePage() {
|
|
|
26
26
|
const handlePinnedClick = (route) => {
|
|
27
27
|
navigate(route);
|
|
28
28
|
};
|
|
29
|
-
return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", "data-testid": "profile-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "profile-heading", children: t('profilePage.title') }), _jsx("p", { className: "text-muted-foreground", children: t('profilePage.userInfo') })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "user", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.userInfo') })] }) }), _jsxs(CardContent, { children: [_jsxs("div", { className: "flex items-center gap-4 mb-6", children: [_jsx(Avatar, { className: "h-16 w-16", children: _jsx(AvatarFallback, { className: "text-2xl font-bold bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xl font-semibold", children: user?.displayName || "User" }), _jsx("p", { className: "text-muted-foreground", children: user?.email || "No email" })] })] }), _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.name') }), _jsx("dd", { "data-testid": "profile-display-name", className: `text-sm font-medium ${!user?.displayName ? "text-muted-foreground" : ""}`, children: user?.displayName || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.email') }), _jsx("dd", { "data-testid": "profile-email", className: `text-sm font-medium ${!user?.email ? "text-muted-foreground" : ""}`, children: user?.email || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.roles') }), _jsx("dd", { "data-testid": "profile-roles", children: _jsx(Badge, { variant: "secondary", children: "
|
|
29
|
+
return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", "data-testid": "profile-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "profile-heading", children: t('profilePage.title') }), _jsx("p", { className: "text-muted-foreground", children: t('profilePage.userInfo') })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "user", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.userInfo') })] }) }), _jsxs(CardContent, { children: [_jsxs("div", { className: "flex items-center gap-4 mb-6", children: [_jsx(Avatar, { className: "h-16 w-16", children: _jsx(AvatarFallback, { className: "text-2xl font-bold bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xl font-semibold", children: user?.displayName || "User" }), _jsx("p", { className: "text-muted-foreground", children: user?.email || "No email" })] })] }), _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.name') }), _jsx("dd", { "data-testid": "profile-display-name", className: `text-sm font-medium ${!user?.displayName ? "text-muted-foreground" : ""}`, children: user?.displayName || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.email') }), _jsx("dd", { "data-testid": "profile-email", className: `text-sm font-medium ${!user?.email ? "text-muted-foreground" : ""}`, children: user?.email || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.roles') }), _jsx("dd", { "data-testid": "profile-roles", className: "flex flex-wrap gap-1 px-2", children: user?.roles && user.roles.length > 0 ? (user.roles.map((role) => (_jsx(Badge, { variant: "secondary", children: role }, role)))) : (_jsx(Badge, { variant: "outline", children: "No roles" })) })] })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "pin", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.pinnedPages') })] }), _jsx(CardDescription, { children: t('profilePage.pinnedPages') })] }), _jsx(CardContent, { children: pinnedItems.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-2", children: t('profilePage.noPinnedPages') })) : (_jsx("ul", { "data-testid": "pinned-pages-list", className: "space-y-1", children: pinnedItems.map((item) => (_jsx("li", { children: _jsxs("button", { "data-testid": `pinned-page-${item.commandId}`, onClick: () => handlePinnedClick(item.route), className: "w-full flex items-center gap-3 text-left px-3 py-2.5 rounded-md hover:bg-accent transition-colors", children: [_jsx(Icon, { name: item.commandIcon || item.moduleIcon || "file-text", className: "h-4 w-4 text-muted-foreground" }), _jsxs("span", { className: "text-sm", children: [item.moduleTitle, " ", _jsx("span", { className: "text-muted-foreground", children: "/" }), " ", item.commandTitle] })] }) }, `${item.moduleId}-${item.commandId}`))) })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "shield", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.auditInfo') })] }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsx("span", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.userId') }), _jsx("span", { "data-testid": "profile-user-id", className: `text-sm font-mono px-2 py-1 rounded ${user?.id ? "bg-muted" : "text-muted-foreground"}`, children: user?.id || "N/A" })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('profilePage.auditInfo') })] })] })] }));
|
|
30
30
|
}
|