@shellui/core 0.2.0 → 0.3.0-beta.1
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 +167 -0
- package/src/features/settings/utils/buildSettingsForPropagation.ts +61 -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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Settings } from '@shellui/sdk';
|
|
3
|
+
import { mergePreferencesIntoSettings } from './mergePreferencesIntoSettings';
|
|
4
|
+
|
|
5
|
+
const baseSettings: Settings = {
|
|
6
|
+
developerFeatures: { enabled: false },
|
|
7
|
+
errorReporting: { enabled: true },
|
|
8
|
+
logging: { namespaces: { shellsdk: false, shellcore: false } },
|
|
9
|
+
appearance: {
|
|
10
|
+
name: 'default',
|
|
11
|
+
displayName: 'Default',
|
|
12
|
+
mode: 'light',
|
|
13
|
+
colorScheme: 'system',
|
|
14
|
+
colors: {
|
|
15
|
+
light: {
|
|
16
|
+
background: '#fff',
|
|
17
|
+
foreground: '#000',
|
|
18
|
+
card: '#fff',
|
|
19
|
+
cardForeground: '#000',
|
|
20
|
+
popover: '#fff',
|
|
21
|
+
popoverForeground: '#000',
|
|
22
|
+
primary: '#000',
|
|
23
|
+
primaryForeground: '#fff',
|
|
24
|
+
secondary: '#eee',
|
|
25
|
+
secondaryForeground: '#000',
|
|
26
|
+
muted: '#eee',
|
|
27
|
+
mutedForeground: '#333',
|
|
28
|
+
accent: '#eee',
|
|
29
|
+
accentForeground: '#000',
|
|
30
|
+
destructive: '#f00',
|
|
31
|
+
destructiveForeground: '#fff',
|
|
32
|
+
border: '#ccc',
|
|
33
|
+
input: '#ccc',
|
|
34
|
+
ring: '#000',
|
|
35
|
+
radius: '0.5rem',
|
|
36
|
+
sidebarBackground: '#fff',
|
|
37
|
+
sidebarForeground: '#000',
|
|
38
|
+
sidebarPrimary: '#000',
|
|
39
|
+
sidebarPrimaryForeground: '#fff',
|
|
40
|
+
sidebarAccent: '#eee',
|
|
41
|
+
sidebarAccentForeground: '#000',
|
|
42
|
+
sidebarBorder: '#ccc',
|
|
43
|
+
sidebarRing: '#000',
|
|
44
|
+
},
|
|
45
|
+
dark: {
|
|
46
|
+
background: '#000',
|
|
47
|
+
foreground: '#fff',
|
|
48
|
+
card: '#000',
|
|
49
|
+
cardForeground: '#fff',
|
|
50
|
+
popover: '#000',
|
|
51
|
+
popoverForeground: '#fff',
|
|
52
|
+
primary: '#fff',
|
|
53
|
+
primaryForeground: '#000',
|
|
54
|
+
secondary: '#111',
|
|
55
|
+
secondaryForeground: '#fff',
|
|
56
|
+
muted: '#111',
|
|
57
|
+
mutedForeground: '#ddd',
|
|
58
|
+
accent: '#111',
|
|
59
|
+
accentForeground: '#fff',
|
|
60
|
+
destructive: '#900',
|
|
61
|
+
destructiveForeground: '#fff',
|
|
62
|
+
border: '#333',
|
|
63
|
+
input: '#333',
|
|
64
|
+
ring: '#fff',
|
|
65
|
+
radius: '0.5rem',
|
|
66
|
+
sidebarBackground: '#000',
|
|
67
|
+
sidebarForeground: '#fff',
|
|
68
|
+
sidebarPrimary: '#fff',
|
|
69
|
+
sidebarPrimaryForeground: '#000',
|
|
70
|
+
sidebarAccent: '#111',
|
|
71
|
+
sidebarAccentForeground: '#fff',
|
|
72
|
+
sidebarBorder: '#333',
|
|
73
|
+
sidebarRing: '#fff',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
language: { code: 'en' },
|
|
78
|
+
region: { timezone: 'UTC' },
|
|
79
|
+
cookieConsent: { acceptedHosts: [], consentedCookieHosts: [] },
|
|
80
|
+
serviceWorker: { enabled: true },
|
|
81
|
+
user: null,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
describe('mergePreferencesIntoSettings', () => {
|
|
85
|
+
it('applies valid preference values', () => {
|
|
86
|
+
const result = mergePreferencesIntoSettings(baseSettings, {
|
|
87
|
+
themeName: 'blue',
|
|
88
|
+
language: 'fr',
|
|
89
|
+
region: 'Europe/Paris',
|
|
90
|
+
colorScheme: 'dark',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result.appearance.name).toBe('blue');
|
|
94
|
+
expect(result.appearance.colorScheme).toBe('dark');
|
|
95
|
+
expect(result.language.code).toBe('fr');
|
|
96
|
+
expect(result.region.timezone).toBe('Europe/Paris');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('keeps current language and color scheme when incoming values are invalid', () => {
|
|
100
|
+
const result = mergePreferencesIntoSettings(baseSettings, {
|
|
101
|
+
language: 'es',
|
|
102
|
+
colorScheme: undefined,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.language.code).toBe('en');
|
|
106
|
+
expect(result.appearance.colorScheme).toBe('system');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Settings } from '@shellui/sdk';
|
|
2
|
+
|
|
3
|
+
export type AppPreferences = {
|
|
4
|
+
themeName?: string;
|
|
5
|
+
language?: string;
|
|
6
|
+
region?: string;
|
|
7
|
+
colorScheme?: 'light' | 'dark' | 'system';
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const mergePreferencesIntoSettings = (
|
|
11
|
+
currentSettings: Settings,
|
|
12
|
+
preferences: AppPreferences,
|
|
13
|
+
): Settings => {
|
|
14
|
+
const hasThemeName =
|
|
15
|
+
typeof preferences.themeName === 'string' && preferences.themeName.trim() !== '';
|
|
16
|
+
const hasLanguage =
|
|
17
|
+
typeof preferences.language === 'string' && preferences.language.trim() !== '';
|
|
18
|
+
const hasRegion = typeof preferences.region === 'string' && preferences.region.trim() !== '';
|
|
19
|
+
const hasColorScheme =
|
|
20
|
+
preferences.colorScheme === 'light' ||
|
|
21
|
+
preferences.colorScheme === 'dark' ||
|
|
22
|
+
preferences.colorScheme === 'system';
|
|
23
|
+
const normalizedColorScheme: 'light' | 'dark' | 'system' = hasColorScheme
|
|
24
|
+
? preferences.colorScheme
|
|
25
|
+
: currentSettings.appearance.colorScheme;
|
|
26
|
+
const normalizedLanguage =
|
|
27
|
+
preferences.language === 'fr' || preferences.language === 'en'
|
|
28
|
+
? preferences.language
|
|
29
|
+
: currentSettings.language.code;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...currentSettings,
|
|
33
|
+
appearance: {
|
|
34
|
+
...currentSettings.appearance,
|
|
35
|
+
...(hasThemeName ? { name: preferences.themeName?.trim() } : {}),
|
|
36
|
+
colorScheme: normalizedColorScheme,
|
|
37
|
+
},
|
|
38
|
+
language: {
|
|
39
|
+
...currentSettings.language,
|
|
40
|
+
...(hasLanguage ? { code: normalizedLanguage } : {}),
|
|
41
|
+
},
|
|
42
|
+
region: {
|
|
43
|
+
...currentSettings.region,
|
|
44
|
+
...(hasRegion ? { timezone: preferences.region?.trim() } : {}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { resolveColorMode } from './resolveColorMode';
|
|
3
|
+
|
|
4
|
+
describe('resolveColorMode', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.unstubAllGlobals();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns explicit dark and light values unchanged', () => {
|
|
10
|
+
expect(resolveColorMode('dark')).toBe('dark');
|
|
11
|
+
expect(resolveColorMode('light')).toBe('light');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('resolves system to dark when media query matches', () => {
|
|
15
|
+
vi.stubGlobal('window', {
|
|
16
|
+
matchMedia: () => ({ matches: true }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(resolveColorMode('system')).toBe('dark');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('resolves system to light when media query does not match', () => {
|
|
23
|
+
vi.stubGlobal('window', {
|
|
24
|
+
matchMedia: () => ({ matches: false }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(resolveColorMode('system')).toBe('light');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const resolveColorMode = (colorScheme: 'light' | 'dark' | 'system'): 'light' | 'dark' => {
|
|
2
|
+
if (colorScheme === 'system' && typeof window !== 'undefined') {
|
|
3
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
4
|
+
}
|
|
5
|
+
return colorScheme === 'dark' ? 'dark' : 'light';
|
|
6
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveLabel } from './resolveLabel';
|
|
3
|
+
|
|
4
|
+
describe('resolveLabel', () => {
|
|
5
|
+
it('returns plain string labels unchanged', () => {
|
|
6
|
+
expect(resolveLabel('Settings', 'en')).toBe('Settings');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns translated label for requested language', () => {
|
|
10
|
+
expect(resolveLabel({ en: 'Settings', fr: 'Parametres' }, 'fr')).toBe('Parametres');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('falls back to english then first available label', () => {
|
|
14
|
+
expect(resolveLabel({ en: 'Settings', fr: 'Parametres' }, 'de')).toBe('Settings');
|
|
15
|
+
expect(resolveLabel({ en: '', fr: '', es: 'Configuracion' }, 'de')).toBe('');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { toAbsoluteFontUrls } from './toAbsoluteFontUrls';
|
|
3
|
+
|
|
4
|
+
describe('toAbsoluteFontUrls', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.unstubAllGlobals();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('keeps absolute urls unchanged and normalizes relative ones', () => {
|
|
10
|
+
vi.stubGlobal('window', {
|
|
11
|
+
location: { origin: 'https://shellui.dev' },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(
|
|
15
|
+
toAbsoluteFontUrls([
|
|
16
|
+
'https://cdn.example.com/font.css',
|
|
17
|
+
'fonts/local.woff2',
|
|
18
|
+
'/fonts/a.woff2',
|
|
19
|
+
]),
|
|
20
|
+
).toEqual([
|
|
21
|
+
'https://cdn.example.com/font.css',
|
|
22
|
+
'https://shellui.dev/fonts/local.woff2',
|
|
23
|
+
'https://shellui.dev/fonts/a.woff2',
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Convert font file URLs to absolute so iframes/modals on other ports or domains can load them. */
|
|
2
|
+
export const toAbsoluteFontUrls = (urls: string[]): string[] => {
|
|
3
|
+
if (typeof window === 'undefined') return urls;
|
|
4
|
+
const origin = window.location.origin;
|
|
5
|
+
|
|
6
|
+
return urls.map((url) => {
|
|
7
|
+
const trimmed = url.trim();
|
|
8
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const path = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
13
|
+
return `${origin}${path}`;
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { toSettingsUser } from './toSettingsUser';
|
|
3
|
+
|
|
4
|
+
describe('toSettingsUser', () => {
|
|
5
|
+
it('returns null when auth user is null', () => {
|
|
6
|
+
expect(toSettingsUser(null)).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('maps auth user fields into settings user shape', () => {
|
|
10
|
+
const result = toSettingsUser({
|
|
11
|
+
id: 'u1',
|
|
12
|
+
email: 'test@example.com',
|
|
13
|
+
name: 'Test User',
|
|
14
|
+
profilePicture: 'https://example.com/avatar.png',
|
|
15
|
+
isStaff: false,
|
|
16
|
+
isCompanyOwner: false,
|
|
17
|
+
authProvider: 'github',
|
|
18
|
+
groups: [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
id: 'u1',
|
|
23
|
+
email: 'test@example.com',
|
|
24
|
+
name: 'Test User',
|
|
25
|
+
profilePicture: 'https://example.com/avatar.png',
|
|
26
|
+
authProvider: 'github',
|
|
27
|
+
groups: null,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const withGroups = toSettingsUser({
|
|
31
|
+
id: 'u1',
|
|
32
|
+
email: 'test@example.com',
|
|
33
|
+
name: 'Test User',
|
|
34
|
+
profilePicture: null,
|
|
35
|
+
isStaff: true,
|
|
36
|
+
isCompanyOwner: false,
|
|
37
|
+
authProvider: 'github',
|
|
38
|
+
groups: ['beta', 'alpha'],
|
|
39
|
+
});
|
|
40
|
+
expect(withGroups).toEqual({
|
|
41
|
+
id: 'u1',
|
|
42
|
+
email: 'test@example.com',
|
|
43
|
+
name: 'Test User',
|
|
44
|
+
profilePicture: null,
|
|
45
|
+
authProvider: 'github',
|
|
46
|
+
groups: ['beta', 'alpha'],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Settings } from '@shellui/sdk';
|
|
2
|
+
import type { AuthUser } from '../../auth/hooks/useAuth';
|
|
3
|
+
|
|
4
|
+
export const toSettingsUser = (user: AuthUser | null): Settings['user'] => {
|
|
5
|
+
if (!user) return null;
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
id: user.id,
|
|
9
|
+
email: user.email,
|
|
10
|
+
name: user.name,
|
|
11
|
+
profilePicture: user.profilePicture,
|
|
12
|
+
authProvider: user.authProvider,
|
|
13
|
+
groups: user.groups.length ? [...user.groups] : null,
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -9,6 +9,20 @@
|
|
|
9
9
|
},
|
|
10
10
|
"navigationError": "Navigation error",
|
|
11
11
|
"navigationNotAllowed": "This URL is not configured in the app navigation.",
|
|
12
|
+
"adminShell": {
|
|
13
|
+
"djangoAdmin": "Django admin"
|
|
14
|
+
},
|
|
15
|
+
"authMenu": {
|
|
16
|
+
"login": "Login",
|
|
17
|
+
"logout": "Logout",
|
|
18
|
+
"profile": "Profile",
|
|
19
|
+
"settings": "Settings",
|
|
20
|
+
"administration": "Administration",
|
|
21
|
+
"noEmail": "No email",
|
|
22
|
+
"userFallback": "User",
|
|
23
|
+
"goToLoginAriaLabel": "Go to login",
|
|
24
|
+
"openAccountMenuAriaLabel": "Open account menu for {{name}}"
|
|
25
|
+
},
|
|
12
26
|
"errorBoundary": {
|
|
13
27
|
"titleChunk": "Page couldn't be loaded",
|
|
14
28
|
"titleGeneric": "Something went wrong",
|
|
@@ -7,14 +7,59 @@
|
|
|
7
7
|
"developer": "Developer"
|
|
8
8
|
},
|
|
9
9
|
"routes": {
|
|
10
|
+
"userAccount": "User account",
|
|
10
11
|
"appearance": "Appearance",
|
|
11
12
|
"languageAndRegion": "Language & region",
|
|
12
13
|
"updateApp": "Software update",
|
|
13
14
|
"advanced": "Advanced",
|
|
14
15
|
"dataPrivacy": "Data Privacy",
|
|
16
|
+
"legalDocuments": "Legal documents",
|
|
15
17
|
"develop": "Develop",
|
|
16
18
|
"serviceWorker": "Service Worker"
|
|
17
19
|
},
|
|
20
|
+
"userAccount": {
|
|
21
|
+
"profile": {
|
|
22
|
+
"avatarAltWithName": "{{name}} avatar",
|
|
23
|
+
"avatarAltDefault": "User avatar",
|
|
24
|
+
"unknownUser": "Unknown user",
|
|
25
|
+
"noEmail": "No email",
|
|
26
|
+
"placeholderInitial": "U"
|
|
27
|
+
},
|
|
28
|
+
"fields": {
|
|
29
|
+
"name": "Name",
|
|
30
|
+
"email": "Email",
|
|
31
|
+
"loginMethod": "Login method",
|
|
32
|
+
"groups": "Groups",
|
|
33
|
+
"noGroups": "None"
|
|
34
|
+
},
|
|
35
|
+
"loginMethods": {
|
|
36
|
+
"unknown": "Unknown",
|
|
37
|
+
"magicLinkEmail": "Magic link (Email)",
|
|
38
|
+
"github": "GitHub"
|
|
39
|
+
},
|
|
40
|
+
"developerDiagnostics": {
|
|
41
|
+
"title": "Developer diagnostics",
|
|
42
|
+
"jwtPayload": "JWT payload",
|
|
43
|
+
"sharedSettingsAccessToken": "Shared settings access token",
|
|
44
|
+
"rawUserSettings": "Raw user settings",
|
|
45
|
+
"copy": "Copy",
|
|
46
|
+
"notSharedForApp": "Not shared for this app.",
|
|
47
|
+
"unableToDecodeJwtPayload": "Unable to decode JWT payload.",
|
|
48
|
+
"noAccessTokenAvailable": "No access token available.",
|
|
49
|
+
"jwtTimestampTooltipAria": "Human-readable date for this timestamp"
|
|
50
|
+
},
|
|
51
|
+
"clipboard": {
|
|
52
|
+
"copyFailedTitle": "Copy failed",
|
|
53
|
+
"clipboardApiUnavailable": "Clipboard API is not available in this environment.",
|
|
54
|
+
"copiedTitle": "Copied",
|
|
55
|
+
"copiedDescription": "{{label}} copied to clipboard.",
|
|
56
|
+
"unableToCopyDiagnostics": "Unable to copy developer diagnostics."
|
|
57
|
+
},
|
|
58
|
+
"actions": {
|
|
59
|
+
"loggingOut": "Logging out...",
|
|
60
|
+
"logout": "Logout"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
18
63
|
"updateApp": {
|
|
19
64
|
"description": "Check your app version and look for updates. When an update is available, you can install it from here.",
|
|
20
65
|
"currentVersion": "Current version",
|
|
@@ -9,6 +9,20 @@
|
|
|
9
9
|
},
|
|
10
10
|
"navigationError": "Erreur de navigation",
|
|
11
11
|
"navigationNotAllowed": "Cette URL n'est pas configurée dans la navigation de l'application.",
|
|
12
|
+
"adminShell": {
|
|
13
|
+
"djangoAdmin": "Admin Django"
|
|
14
|
+
},
|
|
15
|
+
"authMenu": {
|
|
16
|
+
"login": "Connexion",
|
|
17
|
+
"logout": "Se déconnecter",
|
|
18
|
+
"profile": "Profil",
|
|
19
|
+
"settings": "Paramètres",
|
|
20
|
+
"administration": "Administration",
|
|
21
|
+
"noEmail": "Aucun e-mail",
|
|
22
|
+
"userFallback": "Utilisateur",
|
|
23
|
+
"goToLoginAriaLabel": "Aller à la connexion",
|
|
24
|
+
"openAccountMenuAriaLabel": "Ouvrir le menu du compte pour {{name}}"
|
|
25
|
+
},
|
|
12
26
|
"errorBoundary": {
|
|
13
27
|
"titleChunk": "La page n'a pas pu être chargée",
|
|
14
28
|
"titleGeneric": "Une erreur s'est produite",
|
|
@@ -7,14 +7,59 @@
|
|
|
7
7
|
"developer": "Développeur"
|
|
8
8
|
},
|
|
9
9
|
"routes": {
|
|
10
|
+
"userAccount": "Compte utilisateur",
|
|
10
11
|
"appearance": "Apparence",
|
|
11
12
|
"languageAndRegion": "Langue et région",
|
|
12
13
|
"updateApp": "Mise à jour logicielle",
|
|
13
14
|
"advanced": "Avancé",
|
|
14
15
|
"dataPrivacy": "Confidentialité des données",
|
|
16
|
+
"legalDocuments": "Documents légaux",
|
|
15
17
|
"develop": "Développement",
|
|
16
18
|
"serviceWorker": "Service Worker"
|
|
17
19
|
},
|
|
20
|
+
"userAccount": {
|
|
21
|
+
"profile": {
|
|
22
|
+
"avatarAltWithName": "Avatar de {{name}}",
|
|
23
|
+
"avatarAltDefault": "Avatar utilisateur",
|
|
24
|
+
"unknownUser": "Utilisateur inconnu",
|
|
25
|
+
"noEmail": "Aucun e-mail",
|
|
26
|
+
"placeholderInitial": "U"
|
|
27
|
+
},
|
|
28
|
+
"fields": {
|
|
29
|
+
"name": "Nom",
|
|
30
|
+
"email": "E-mail",
|
|
31
|
+
"loginMethod": "Méthode de connexion",
|
|
32
|
+
"groups": "Groupes",
|
|
33
|
+
"noGroups": "Aucun"
|
|
34
|
+
},
|
|
35
|
+
"loginMethods": {
|
|
36
|
+
"unknown": "Inconnue",
|
|
37
|
+
"magicLinkEmail": "Lien magique (e-mail)",
|
|
38
|
+
"github": "GitHub"
|
|
39
|
+
},
|
|
40
|
+
"developerDiagnostics": {
|
|
41
|
+
"title": "Diagnostics développeur",
|
|
42
|
+
"jwtPayload": "Payload JWT",
|
|
43
|
+
"sharedSettingsAccessToken": "Jeton d'accès des paramètres partagés",
|
|
44
|
+
"rawUserSettings": "Paramètres utilisateur bruts",
|
|
45
|
+
"copy": "Copier",
|
|
46
|
+
"notSharedForApp": "Non partagé pour cette application.",
|
|
47
|
+
"unableToDecodeJwtPayload": "Impossible de décoder le payload JWT.",
|
|
48
|
+
"noAccessTokenAvailable": "Aucun jeton d'accès disponible.",
|
|
49
|
+
"jwtTimestampTooltipAria": "Date lisible pour cet horodatage"
|
|
50
|
+
},
|
|
51
|
+
"clipboard": {
|
|
52
|
+
"copyFailedTitle": "Échec de la copie",
|
|
53
|
+
"clipboardApiUnavailable": "L'API Clipboard n'est pas disponible dans cet environnement.",
|
|
54
|
+
"copiedTitle": "Copié",
|
|
55
|
+
"copiedDescription": "{{label}} copié dans le presse-papiers.",
|
|
56
|
+
"unableToCopyDiagnostics": "Impossible de copier les diagnostics développeur."
|
|
57
|
+
},
|
|
58
|
+
"actions": {
|
|
59
|
+
"loggingOut": "Déconnexion...",
|
|
60
|
+
"logout": "Se déconnecter"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
18
63
|
"updateApp": {
|
|
19
64
|
"description": "Consultez la version de l'application et vérifiez les mises à jour. Lorsqu'une mise à jour est disponible, vous pouvez l'installer depuis ici.",
|
|
20
65
|
"currentVersion": "Version actuelle",
|
package/src/index.css
CHANGED
|
@@ -431,3 +431,40 @@
|
|
|
431
431
|
width: 100%;
|
|
432
432
|
}
|
|
433
433
|
}
|
|
434
|
+
|
|
435
|
+
/* Auth account dropdown menu animation (LoginButton) */
|
|
436
|
+
@keyframes auth-menu-in {
|
|
437
|
+
from {
|
|
438
|
+
opacity: 0;
|
|
439
|
+
transform: scale(0.985);
|
|
440
|
+
}
|
|
441
|
+
to {
|
|
442
|
+
opacity: 1;
|
|
443
|
+
transform: scale(1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@keyframes auth-menu-out {
|
|
448
|
+
from {
|
|
449
|
+
opacity: 1;
|
|
450
|
+
transform: scale(1);
|
|
451
|
+
}
|
|
452
|
+
to {
|
|
453
|
+
opacity: 0;
|
|
454
|
+
transform: scale(0.985);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
[data-auth-menu-content] {
|
|
459
|
+
transform-origin: var(--radix-dropdown-menu-content-transform-origin);
|
|
460
|
+
will-change: opacity, transform;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
[data-auth-menu-content][data-state='open'] {
|
|
464
|
+
animation: auth-menu-in 0.16s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
[data-auth-menu-content][data-state='closed'] {
|
|
468
|
+
animation: auth-menu-out 0.12s cubic-bezier(0.4, 0, 1, 1) both;
|
|
469
|
+
pointer-events: none;
|
|
470
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,13 +17,19 @@ export type {
|
|
|
17
17
|
ThemeColors,
|
|
18
18
|
DrawerPosition,
|
|
19
19
|
LayoutType,
|
|
20
|
+
BackendType,
|
|
21
|
+
BackendConfig,
|
|
20
22
|
CookieConsentCategory,
|
|
21
23
|
CookieDefinition,
|
|
22
24
|
CookieConsentConfig,
|
|
25
|
+
LegalDocumentsConfig,
|
|
23
26
|
} from './features/config/types.js';
|
|
24
27
|
export { useConfig } from './features/config/useConfig.js';
|
|
28
|
+
export { useAuth } from './features/auth/hooks/useAuth.js';
|
|
29
|
+
export { AuthProvider } from './features/auth/AuthProvider.js';
|
|
25
30
|
export { ConfigProvider } from './features/config/ConfigProvider.js';
|
|
26
31
|
export type { ConfigContextValue, ConfigProviderProps } from './features/config/ConfigProvider.js';
|
|
32
|
+
export type { AuthSession } from './features/auth/hooks/useAuth.js';
|
|
27
33
|
export { default as urls } from './constants/urls.js';
|
|
28
34
|
export {
|
|
29
35
|
getCookieConsentAccepted,
|
|
@@ -1,14 +1,45 @@
|
|
|
1
1
|
import { ContentView } from '../../components/ContentView';
|
|
2
2
|
import { useNavigationItems } from '../hooks/useNavigationItems';
|
|
3
3
|
import { NotFoundView } from './NotFoundView';
|
|
4
|
+
import { Navigate, useLocation } from 'react-router';
|
|
5
|
+
import urls from '../../constants/urls';
|
|
6
|
+
import { useAuth } from '../../features/auth/hooks/useAuth';
|
|
7
|
+
import { RouteFallback } from './RouteFallback';
|
|
4
8
|
|
|
5
9
|
export const NavigationItemRoute = () => {
|
|
6
|
-
const
|
|
10
|
+
const location = useLocation();
|
|
11
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
12
|
+
const { url, currentItem, isRootFallback } = useNavigationItems();
|
|
13
|
+
|
|
14
|
+
if (isRootFallback) {
|
|
15
|
+
return (
|
|
16
|
+
<Navigate
|
|
17
|
+
to="/"
|
|
18
|
+
replace
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
7
22
|
|
|
8
23
|
if (!currentItem) {
|
|
9
24
|
return <NotFoundView />;
|
|
10
25
|
}
|
|
11
26
|
|
|
27
|
+
if (currentItem.requiresAuth) {
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return <RouteFallback />;
|
|
30
|
+
}
|
|
31
|
+
if (!isAuthenticated) {
|
|
32
|
+
const next = `${location.pathname}${location.search}`;
|
|
33
|
+
const loginUrl = `${urls.login}?next=${encodeURIComponent(next)}`;
|
|
34
|
+
return (
|
|
35
|
+
<Navigate
|
|
36
|
+
to={loginUrl}
|
|
37
|
+
replace
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
return (
|
|
13
44
|
<ContentView
|
|
14
45
|
url={url}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next';
|
|
2
2
|
import { shellui } from '@shellui/sdk';
|
|
3
3
|
import { useConfig } from '../../features/config/useConfig';
|
|
4
|
-
import { getNavPathPrefix } from '../../features/layouts/utils';
|
|
4
|
+
import { filterNavigationForAuthState, getNavPathPrefix } from '../../features/layouts/utils';
|
|
5
5
|
import type { NavigationItem, NavigationGroup } from '../../features/config/types';
|
|
6
|
+
import { useAuth } from '../../features/auth/hooks/useAuth';
|
|
7
|
+
import { useSettings } from '../../features/settings/hooks/useSettings';
|
|
6
8
|
|
|
7
9
|
const flattenNavigationItems = (
|
|
8
10
|
navigation: (NavigationItem | NavigationGroup)[],
|
|
@@ -18,6 +20,8 @@ const flattenNavigationItems = (
|
|
|
18
20
|
|
|
19
21
|
export const NotFoundView = () => {
|
|
20
22
|
const { config } = useConfig();
|
|
23
|
+
const { isAuthenticated } = useAuth();
|
|
24
|
+
const { settings } = useSettings();
|
|
21
25
|
const { i18n } = useTranslation();
|
|
22
26
|
const currentLanguage = i18n.language || 'en';
|
|
23
27
|
|
|
@@ -31,7 +35,13 @@ export const NotFoundView = () => {
|
|
|
31
35
|
|
|
32
36
|
const navItems =
|
|
33
37
|
config?.navigation && config.navigation.length > 0
|
|
34
|
-
? flattenNavigationItems(
|
|
38
|
+
? flattenNavigationItems(
|
|
39
|
+
filterNavigationForAuthState(
|
|
40
|
+
config.navigation,
|
|
41
|
+
isAuthenticated,
|
|
42
|
+
settings.developerFeatures.enabled,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
35
45
|
.filter((item) => !item.hidden)
|
|
36
46
|
.filter((item, index, self) => index === self.findIndex((i) => i.path === item.path))
|
|
37
47
|
: [];
|
|
@@ -62,7 +72,7 @@ export const NotFoundView = () => {
|
|
|
62
72
|
>
|
|
63
73
|
{navItems.map((item, index) => (
|
|
64
74
|
<span
|
|
65
|
-
key={item.path || 'root'}
|
|
75
|
+
key={`${item.path || 'root'}-${item.url}-${item.openIn || 'default'}-${index}`}
|
|
66
76
|
className="inline-flex items-center gap-x-2"
|
|
67
77
|
>
|
|
68
78
|
{index > 0 && (
|