@shellui/core 0.2.0 → 0.3.0-beta.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/package.json +9 -4
- package/src/app.tsx +12 -9
- package/src/components/ui/badge.tsx +35 -0
- package/src/components/ui/dropdown-menu.tsx +94 -0
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/constants/urls.ts +8 -0
- package/src/features/admin/AdminView.tsx +154 -0
- package/src/features/admin/components/AdminForbiddenAccess.tsx +10 -0
- package/src/features/auth/AuthProvider.tsx +464 -0
- package/src/features/auth/backends/index.ts +41 -0
- package/src/features/auth/backends/shellui.ts +278 -0
- package/src/features/auth/backends/supabase.ts +300 -0
- package/src/features/auth/backends/types.ts +30 -0
- package/src/features/auth/components/LoginButton.tsx +360 -0
- package/src/features/auth/components/LoginButtonIcons.tsx +48 -0
- package/src/features/auth/components/LoginView.tsx +721 -0
- package/src/features/auth/components/OAuthCallbackView.tsx +119 -0
- package/src/features/auth/hooks/useAuth.tsx +37 -0
- package/src/features/auth/types.ts +51 -0
- package/src/features/auth/utils/buildSessionFromParams.spec.ts +61 -0
- package/src/features/auth/utils/buildSessionFromParams.ts +79 -0
- package/src/features/auth/utils/clearStoredAuthSession.spec.ts +23 -0
- package/src/features/auth/utils/clearStoredAuthSession.ts +10 -0
- package/src/features/auth/utils/clientLoginContext.spec.ts +83 -0
- package/src/features/auth/utils/clientLoginContext.ts +89 -0
- package/src/features/auth/utils/decodeJwtPayload.spec.ts +17 -0
- package/src/features/auth/utils/decodeJwtPayload.ts +24 -0
- package/src/features/auth/utils/formatProviderLabel.spec.ts +19 -0
- package/src/features/auth/utils/formatProviderLabel.ts +11 -0
- package/src/features/auth/utils/getOAuthProviderCandidates.spec.ts +16 -0
- package/src/features/auth/utils/getOAuthProviderCandidates.ts +15 -0
- package/src/features/auth/utils/getPreferredBackendProvider.spec.ts +15 -0
- package/src/features/auth/utils/getPreferredBackendProvider.ts +10 -0
- package/src/features/auth/utils/getProviderVisual.spec.ts +30 -0
- package/src/features/auth/utils/getProviderVisual.ts +83 -0
- package/src/features/auth/utils/getUserFromSdkSettings.spec.ts +32 -0
- package/src/features/auth/utils/getUserFromSdkSettings.ts +13 -0
- package/src/features/auth/utils/index.ts +21 -0
- package/src/features/auth/utils/isLoginMethod.spec.ts +18 -0
- package/src/features/auth/utils/isLoginMethod.ts +5 -0
- package/src/features/auth/utils/isSessionExpired.spec.ts +23 -0
- package/src/features/auth/utils/isSessionExpired.ts +5 -0
- package/src/features/auth/utils/normalizeAuthSettings.spec.ts +60 -0
- package/src/features/auth/utils/normalizeAuthSettings.ts +71 -0
- package/src/features/auth/utils/normalizeNextPath.spec.ts +21 -0
- package/src/features/auth/utils/normalizeNextPath.ts +12 -0
- package/src/features/auth/utils/normalizeRedirectPath.spec.ts +12 -0
- package/src/features/auth/utils/normalizeRedirectPath.ts +3 -0
- package/src/features/auth/utils/persistAuthSession.spec.ts +35 -0
- package/src/features/auth/utils/persistAuthSession.ts +12 -0
- package/src/features/auth/utils/readStoredAuthSession.spec.ts +34 -0
- package/src/features/auth/utils/readStoredAuthSession.ts +14 -0
- package/src/features/auth/utils/toAuthSessionFromSettingsUser.spec.ts +76 -0
- package/src/features/auth/utils/toAuthSessionFromSettingsUser.ts +36 -0
- package/src/features/config/types.ts +55 -0
- package/src/features/layouts/AppLayout.tsx +8 -6
- package/src/features/layouts/appbar/AppBarLayout.tsx +42 -23
- package/src/features/layouts/fullscreen/FullscreenLayout.tsx +3 -2
- package/src/features/layouts/sidebar/SidebarInner.tsx +7 -5
- package/src/features/layouts/sidebar/SidebarLayout.tsx +16 -3
- package/src/features/layouts/utils.ts +54 -0
- package/src/features/layouts/windows/WindowsLayout.tsx +22 -4
- package/src/features/legal/LegalDocumentContent.tsx +102 -0
- package/src/features/legal/LegalDocumentView.tsx +42 -0
- package/src/features/legal/LegalDocumentsIndexView.tsx +51 -0
- package/src/features/legal/LegalDocumentsLinks.tsx +29 -0
- package/src/features/legal/legalDocuments.ts +62 -0
- package/src/features/settings/SettingsIcons.tsx +20 -0
- package/src/features/settings/SettingsProvider.tsx +347 -245
- package/src/features/settings/SettingsRoutes.tsx +8 -0
- package/src/features/settings/SettingsView.tsx +43 -8
- package/src/features/settings/components/Develop.tsx +2 -2
- package/src/features/settings/components/LegalDocumentsPanel.tsx +46 -0
- package/src/features/settings/components/UserIcon.tsx +20 -0
- package/src/features/settings/components/UserSettingsPanel.tsx +438 -0
- package/src/features/settings/components/createUserSettingsRoute.tsx +43 -0
- package/src/features/settings/utils/buildSettingsForPropagation.spec.ts +143 -0
- package/src/features/settings/utils/buildSettingsForPropagation.ts +55 -0
- package/src/features/settings/utils/flattenNavigationItems.spec.ts +17 -0
- package/src/features/settings/utils/flattenNavigationItems.ts +12 -0
- package/src/features/settings/utils/getAvailableThemesForSettings.spec.ts +15 -0
- package/src/features/settings/utils/getAvailableThemesForSettings.ts +16 -0
- package/src/features/settings/utils/getBrowserTimezone.spec.ts +11 -0
- package/src/features/settings/utils/getBrowserTimezone.ts +7 -0
- package/src/features/settings/utils/getPreferenceSnapshot.spec.ts +35 -0
- package/src/features/settings/utils/getPreferenceSnapshot.ts +10 -0
- package/src/features/settings/utils/getResolvedAppearanceForSettings.spec.ts +97 -0
- package/src/features/settings/utils/getResolvedAppearanceForSettings.ts +48 -0
- package/src/features/settings/utils/index.ts +12 -0
- package/src/features/settings/utils/isSameUser.spec.ts +35 -0
- package/src/features/settings/utils/isSameUser.ts +17 -0
- package/src/features/settings/utils/mergePreferencesIntoSettings.spec.ts +108 -0
- package/src/features/settings/utils/mergePreferencesIntoSettings.ts +47 -0
- package/src/features/settings/utils/resolveColorMode.spec.ts +29 -0
- package/src/features/settings/utils/resolveColorMode.ts +6 -0
- package/src/features/settings/utils/resolveLabel.spec.ts +17 -0
- package/src/features/settings/utils/resolveLabel.ts +7 -0
- package/src/features/settings/utils/toAbsoluteFontUrls.spec.ts +26 -0
- package/src/features/settings/utils/toAbsoluteFontUrls.ts +15 -0
- package/src/features/settings/utils/toSettingsUser.spec.ts +49 -0
- package/src/features/settings/utils/toSettingsUser.ts +15 -0
- package/src/i18n/translations/en/common.json +14 -0
- package/src/i18n/translations/en/settings.json +45 -0
- package/src/i18n/translations/fr/common.json +14 -0
- package/src/i18n/translations/fr/settings.json +45 -0
- package/src/index.css +37 -0
- package/src/index.ts +6 -0
- package/src/routes/components/NavigationItemRoute.tsx +32 -1
- package/src/routes/components/NotFoundView.tsx +13 -3
- package/src/routes/hooks/useNavigationItems.ts +19 -4
- package/src/routes/routes.tsx +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shellui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta.0",
|
|
4
4
|
"description": "ShellUI Core - Core React application runtime",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -39,14 +39,18 @@
|
|
|
39
39
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
40
40
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
41
41
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
42
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
42
43
|
"@radix-ui/react-separator": "^1.1.8",
|
|
43
44
|
"@radix-ui/react-slot": "^1.2.4",
|
|
44
45
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
45
46
|
"@sentry/react": "^10.39.0",
|
|
47
|
+
"@supabase/supabase-js": "^2.99.3",
|
|
46
48
|
"class-variance-authority": "^0.7.1",
|
|
47
49
|
"clsx": "^2.1.1",
|
|
48
50
|
"i18next": "^25.8.11",
|
|
49
51
|
"react-i18next": "^16.5.4",
|
|
52
|
+
"react-icons": "^5.6.0",
|
|
53
|
+
"react-markdown": "^10.1.0",
|
|
50
54
|
"react-router": "^7.13.0",
|
|
51
55
|
"roarr": "^7.21.4",
|
|
52
56
|
"sonner": "^2.0.7",
|
|
@@ -58,7 +62,7 @@
|
|
|
58
62
|
"workbox-routing": "^7.4.0",
|
|
59
63
|
"workbox-strategies": "^7.4.0",
|
|
60
64
|
"workbox-window": "^7.4.0",
|
|
61
|
-
"@shellui/sdk": "0.
|
|
65
|
+
"@shellui/sdk": "0.3.0-beta.0"
|
|
62
66
|
},
|
|
63
67
|
"peerDependencies": {
|
|
64
68
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -74,9 +78,10 @@
|
|
|
74
78
|
"react": "^19.2.4",
|
|
75
79
|
"react-dom": "^19.2.4",
|
|
76
80
|
"tailwindcss-animate": "^1.0.7",
|
|
77
|
-
"typescript": "^5.9.3"
|
|
81
|
+
"typescript": "^5.9.3",
|
|
82
|
+
"vitest": "^4.0.18"
|
|
78
83
|
},
|
|
79
84
|
"scripts": {
|
|
80
|
-
"test": "
|
|
85
|
+
"test": "node scripts/test.js"
|
|
81
86
|
}
|
|
82
87
|
}
|
package/src/app.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { ThemeProvider } from './features/theme/ThemeProvider';
|
|
|
9
9
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
10
10
|
import { DialogProvider } from './features/alertDialog/DialogContext';
|
|
11
11
|
import { CookieConsentModal } from './features/cookieConsent/CookieConsentModal';
|
|
12
|
+
import { AuthProvider } from './features/auth/AuthProvider';
|
|
12
13
|
import './features/sentry/initSentry';
|
|
13
14
|
import './i18n/config'; // Initialize i18n
|
|
14
15
|
import './index.css';
|
|
@@ -103,15 +104,17 @@ const App = () => {
|
|
|
103
104
|
|
|
104
105
|
return (
|
|
105
106
|
<ConfigProvider>
|
|
106
|
-
<
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
<AuthProvider>
|
|
108
|
+
<SettingsProvider>
|
|
109
|
+
<ThemeProvider>
|
|
110
|
+
<I18nProvider>
|
|
111
|
+
<DialogProvider>
|
|
112
|
+
<AppContent />
|
|
113
|
+
</DialogProvider>
|
|
114
|
+
</I18nProvider>
|
|
115
|
+
</ThemeProvider>
|
|
116
|
+
</SettingsProvider>
|
|
117
|
+
</AuthProvider>
|
|
115
118
|
</ConfigProvider>
|
|
116
119
|
);
|
|
117
120
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
|
11
|
+
secondary:
|
|
12
|
+
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
13
|
+
outline: 'text-foreground',
|
|
14
|
+
muted: 'border-border bg-muted/50 text-muted-foreground',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: 'outline',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export interface BadgeProps
|
|
24
|
+
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
|
25
|
+
|
|
26
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from 'react';
|
|
2
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Z_INDEX } from '../../lib/z-index';
|
|
5
|
+
|
|
6
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
7
|
+
|
|
8
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
9
|
+
|
|
10
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
11
|
+
|
|
12
|
+
const DropdownMenuContent = forwardRef<
|
|
13
|
+
ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
14
|
+
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
15
|
+
>(({ className, sideOffset = 6, forceMount, ...props }, ref) => (
|
|
16
|
+
<DropdownMenuPrimitive.Portal>
|
|
17
|
+
<DropdownMenuPrimitive.Content
|
|
18
|
+
ref={ref}
|
|
19
|
+
forceMount={forceMount}
|
|
20
|
+
sideOffset={sideOffset}
|
|
21
|
+
className={cn(
|
|
22
|
+
'z-50 min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md',
|
|
23
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
24
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
25
|
+
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
26
|
+
'data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1',
|
|
27
|
+
'data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
style={{ zIndex: Z_INDEX.MODAL_CONTENT }}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
</DropdownMenuPrimitive.Portal>
|
|
34
|
+
));
|
|
35
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
36
|
+
|
|
37
|
+
const DropdownMenuItem = forwardRef<
|
|
38
|
+
ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
39
|
+
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
|
|
40
|
+
>(({ className, ...props }, ref) => (
|
|
41
|
+
<DropdownMenuPrimitive.Item
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
'relative flex items-center gap-2 cursor-pointer select-none rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
|
|
45
|
+
'focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
));
|
|
51
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
52
|
+
|
|
53
|
+
const DropdownMenuLabel = forwardRef<
|
|
54
|
+
ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
55
|
+
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
|
|
56
|
+
>(({ className, ...props }, ref) => (
|
|
57
|
+
<DropdownMenuPrimitive.Label
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
));
|
|
63
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
64
|
+
|
|
65
|
+
const DropdownMenuSeparator = forwardRef<
|
|
66
|
+
ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
67
|
+
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
68
|
+
>(({ className, ...props }, ref) => (
|
|
69
|
+
<DropdownMenuPrimitive.Separator
|
|
70
|
+
ref={ref}
|
|
71
|
+
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
));
|
|
75
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
76
|
+
|
|
77
|
+
const DropdownMenuShortcut = ({ className, ...props }: ComponentPropsWithoutRef<'span'>) => (
|
|
78
|
+
<span
|
|
79
|
+
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
DropdownMenu,
|
|
87
|
+
DropdownMenuPortal,
|
|
88
|
+
DropdownMenuTrigger,
|
|
89
|
+
DropdownMenuContent,
|
|
90
|
+
DropdownMenuLabel,
|
|
91
|
+
DropdownMenuItem,
|
|
92
|
+
DropdownMenuSeparator,
|
|
93
|
+
DropdownMenuShortcut,
|
|
94
|
+
};
|
|
@@ -129,7 +129,7 @@ const SidebarGroupContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
|
|
|
129
129
|
SidebarGroupContent.displayName = 'SidebarGroupContent';
|
|
130
130
|
|
|
131
131
|
const sidebarMenuButtonVariants = cva(
|
|
132
|
-
'flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:bg-sidebar-accent focus-visible:text-sidebar-accent-foreground focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground',
|
|
132
|
+
'flex w-full appearance-none items-center gap-2 overflow-hidden rounded-md bg-transparent p-2 text-left text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:filter-none focus-visible:bg-sidebar-accent focus-visible:text-sidebar-accent-foreground focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground',
|
|
133
133
|
{
|
|
134
134
|
variants: {
|
|
135
135
|
variant: {
|
package/src/constants/urls.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
settings: '/__settings',
|
|
3
3
|
cookiePreferences: '/__cookie-preferences',
|
|
4
|
+
login: '/login',
|
|
5
|
+
loginCallback: '/login/callback',
|
|
6
|
+
admin: '/admin',
|
|
7
|
+
legalDocuments: '/legal',
|
|
8
|
+
legalPrivacyPolicy: '/legal/privacy-policy',
|
|
9
|
+
legalTermsOfService: '/legal/terms-of-service',
|
|
10
|
+
legalNotice: '/legal/legal-notice',
|
|
11
|
+
legalDataProcessingAgreement: '/legal/data-processing-agreement',
|
|
4
12
|
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Outlet, Route, Routes, useLocation, useNavigate } from 'react-router';
|
|
4
|
+
import { Button } from '../../components/ui/button';
|
|
5
|
+
import urls from '../../constants/urls';
|
|
6
|
+
import { ContentView } from '../../components/ContentView';
|
|
7
|
+
import { LoginButton } from '../auth/components/LoginButton';
|
|
8
|
+
import { useAuth } from '../auth/hooks/useAuth';
|
|
9
|
+
import { useConfig } from '../config/useConfig';
|
|
10
|
+
import type { NavigationItem } from '../config/types';
|
|
11
|
+
import { getBaseUrlWithoutHash } from '../layouts/utils';
|
|
12
|
+
import { AdminForbiddenAccess } from './components/AdminForbiddenAccess';
|
|
13
|
+
import { AppLayout } from '../layouts/AppLayout';
|
|
14
|
+
|
|
15
|
+
/** Admin microfrontend uses hash routes (e.g. createHashRouter); sync shell `/admin/...` with iframe `#/...`. */
|
|
16
|
+
function buildAdminIframeSrc(
|
|
17
|
+
baseAdminContentUrl: string,
|
|
18
|
+
normalizedAdminPath: string,
|
|
19
|
+
pathname: string,
|
|
20
|
+
search: string,
|
|
21
|
+
): string {
|
|
22
|
+
const pathAfterAdmin = pathname.startsWith(normalizedAdminPath)
|
|
23
|
+
? pathname.slice(normalizedAdminPath.length)
|
|
24
|
+
: '';
|
|
25
|
+
const segment = pathAfterAdmin.replace(/^\/+|\/+$/g, '');
|
|
26
|
+
const hashRoute = segment ? `/${segment}` : '/';
|
|
27
|
+
const originBase = getBaseUrlWithoutHash(baseAdminContentUrl).replace(/\/+$/, '');
|
|
28
|
+
return `${originBase}/#${hashRoute}${search}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AdminAccessGuard = ({ allow }: { allow: boolean }) => {
|
|
32
|
+
if (!allow) {
|
|
33
|
+
return <AdminForbiddenAccess />;
|
|
34
|
+
}
|
|
35
|
+
return <Outlet />;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const AdminView = () => {
|
|
39
|
+
const { t } = useTranslation('common');
|
|
40
|
+
const navigate = useNavigate();
|
|
41
|
+
const location = useLocation();
|
|
42
|
+
const { config } = useConfig();
|
|
43
|
+
const { user } = useAuth();
|
|
44
|
+
const isStaff = Boolean(user?.isStaff);
|
|
45
|
+
const canOpenAdminPanel = Boolean(user?.isStaff || user?.isCompanyOwner);
|
|
46
|
+
const djangoAdminHref = useMemo(() => {
|
|
47
|
+
if (config.backend?.type !== 'shellui' || !config.backend.url?.trim()) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return `${config.backend.url.replace(/\/+$/, '')}/admin`;
|
|
51
|
+
}, [config.backend?.type, config.backend?.url]);
|
|
52
|
+
const configuredAdminPathname = config.backend?.adminPathname?.trim();
|
|
53
|
+
const adminPath =
|
|
54
|
+
configuredAdminPathname && configuredAdminPathname.startsWith('/')
|
|
55
|
+
? configuredAdminPathname
|
|
56
|
+
: urls.admin;
|
|
57
|
+
const baseAdminContentUrl = config.backend?.adminUrl?.trim() || urls.settings;
|
|
58
|
+
const initialAdminContentUrlRef = useRef<string | null>(null);
|
|
59
|
+
if (!initialAdminContentUrlRef.current) {
|
|
60
|
+
const normalizedAdminPath = adminPath.replace(/\/+$/, '');
|
|
61
|
+
initialAdminContentUrlRef.current = buildAdminIframeSrc(
|
|
62
|
+
baseAdminContentUrl,
|
|
63
|
+
normalizedAdminPath,
|
|
64
|
+
location.pathname,
|
|
65
|
+
location.search,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const initialAdminContentUrl = initialAdminContentUrlRef.current;
|
|
69
|
+
const adminContentItem = useMemo<NavigationItem>(
|
|
70
|
+
() => ({
|
|
71
|
+
label: 'Settings',
|
|
72
|
+
path: adminPath.replace(/^\/+/, ''),
|
|
73
|
+
url: baseAdminContentUrl,
|
|
74
|
+
/** Required so ContentView maps iframe hash → shell path (`/admin`, `/admin/users`, …). */
|
|
75
|
+
useHashRouter: true,
|
|
76
|
+
}),
|
|
77
|
+
[adminPath, baseAdminContentUrl],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (config?.title) {
|
|
82
|
+
document.title = `Administration | ${config.title}`;
|
|
83
|
+
} else {
|
|
84
|
+
document.title = 'Administration';
|
|
85
|
+
}
|
|
86
|
+
}, [config?.title]);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<AppLayout>
|
|
90
|
+
<div className="flex h-full w-full flex-col overflow-hidden bg-background">
|
|
91
|
+
<header className="flex h-12 shrink-0 items-center justify-between border-b bg-card px-2 md:px-3">
|
|
92
|
+
<Button
|
|
93
|
+
type="button"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
size="sm"
|
|
96
|
+
onClick={() => navigate('/')}
|
|
97
|
+
>
|
|
98
|
+
<svg
|
|
99
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
100
|
+
viewBox="0 0 24 24"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
strokeWidth="2"
|
|
104
|
+
strokeLinecap="round"
|
|
105
|
+
strokeLinejoin="round"
|
|
106
|
+
className="h-4 w-4"
|
|
107
|
+
aria-hidden
|
|
108
|
+
>
|
|
109
|
+
<path d="m15 18-6-6 6-6" />
|
|
110
|
+
</svg>
|
|
111
|
+
Back to home
|
|
112
|
+
</Button>
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
{isStaff && djangoAdminHref ? (
|
|
115
|
+
<Button
|
|
116
|
+
variant="outline"
|
|
117
|
+
size="sm"
|
|
118
|
+
asChild
|
|
119
|
+
>
|
|
120
|
+
<a
|
|
121
|
+
href={djangoAdminHref}
|
|
122
|
+
target="_blank"
|
|
123
|
+
rel="noopener noreferrer"
|
|
124
|
+
>
|
|
125
|
+
{t('adminShell.djangoAdmin')}
|
|
126
|
+
</a>
|
|
127
|
+
</Button>
|
|
128
|
+
) : null}
|
|
129
|
+
<LoginButton
|
|
130
|
+
variant="appbar"
|
|
131
|
+
logoutOnly
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</header>
|
|
135
|
+
<main className="flex min-h-0 flex-1">
|
|
136
|
+
<Routes>
|
|
137
|
+
<Route element={<AdminAccessGuard allow={canOpenAdminPanel} />}>
|
|
138
|
+
<Route
|
|
139
|
+
path="*"
|
|
140
|
+
element={
|
|
141
|
+
<ContentView
|
|
142
|
+
url={initialAdminContentUrl}
|
|
143
|
+
pathPrefix={adminPath.replace(/^\/+/, '')}
|
|
144
|
+
navItem={adminContentItem}
|
|
145
|
+
/>
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
</Route>
|
|
149
|
+
</Routes>
|
|
150
|
+
</main>
|
|
151
|
+
</div>
|
|
152
|
+
</AppLayout>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const AdminForbiddenAccess = () => {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex h-full w-full items-center justify-center p-6">
|
|
4
|
+
<div className="max-w-md text-center">
|
|
5
|
+
<h1 className="text-xl font-semibold">Access forbidden</h1>
|
|
6
|
+
<p className="mt-2 text-sm text-muted-foreground">Contact your administrator.</p>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
};
|