@shellui/core 0.1.0 → 0.2.0-alpha.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 +4 -2
- package/src/components/AppPathView.tsx +31 -0
- package/src/components/ContentView.tsx +14 -6
- package/src/components/HomeView.tsx +9 -2
- package/src/components/IndexRoute.tsx +37 -0
- package/src/components/NotFoundView.tsx +3 -2
- package/src/components/ViewRoute.tsx +11 -7
- package/src/components/ui/tooltip.tsx +52 -0
- package/src/constants/urls.ts +2 -0
- package/src/features/config/ConfigProvider.ts +20 -76
- package/src/features/config/shellui-config.d.ts +13 -0
- package/src/features/config/types.ts +14 -3
- package/src/features/config/useConfig.ts +1 -10
- package/src/features/cookieConsent/cookieConsent.ts +2 -4
- package/src/features/layouts/AppBarLayout.tsx +260 -0
- package/src/features/layouts/AppLayout.tsx +6 -0
- package/src/features/layouts/DefaultLayout.tsx +25 -17
- package/src/features/layouts/OverlayShell.tsx +19 -8
- package/src/features/layouts/WindowsLayout.tsx +11 -9
- package/src/features/layouts/utils.ts +44 -0
- package/src/features/sentry/initSentry.ts +82 -12
- package/src/features/settings/SettingsProvider.tsx +2 -1
- package/src/features/settings/SettingsView.tsx +79 -15
- package/src/features/settings/components/Advanced.tsx +17 -2
- package/src/features/settings/components/ApplicationSettingsPanel.tsx +25 -0
- package/src/features/settings/components/Develop.tsx +68 -4
- package/src/i18n/translations/en/common.json +5 -0
- package/src/i18n/translations/en/settings.json +3 -1
- package/src/i18n/translations/fr/common.json +5 -0
- package/src/i18n/translations/fr/settings.json +3 -1
- package/src/index.css +10 -0
- package/src/lib/z-index.ts +2 -0
- package/src/router/routes.tsx +18 -5
- package/tailwind.config.js +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getCookieConsentAccepted } from '../cookieConsent/cookieConsent';
|
|
2
|
+
import shelluiConfig from '@shellui/config';
|
|
2
3
|
|
|
3
4
|
const SETTINGS_KEY = 'shellui:settings';
|
|
4
5
|
|
|
@@ -19,17 +20,10 @@ function isErrorReportingEnabled(): boolean {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
type SentryGlobals = {
|
|
23
|
-
__SHELLUI_SENTRY_DSN__?: string;
|
|
24
|
-
__SHELLUI_SENTRY_ENVIRONMENT__?: string;
|
|
25
|
-
__SHELLUI_SENTRY_RELEASE__?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
23
|
/**
|
|
29
24
|
* Initialize error reporting only in production when configured and user has not disabled it.
|
|
30
25
|
* Lazy-loads @sentry/react only when needed so the bundle is not loaded when Sentry is unused.
|
|
31
|
-
* Reads
|
|
32
|
-
* and __SHELLUI_SENTRY_RELEASE__ (injected at build time) and user preference from settings.
|
|
26
|
+
* Reads Sentry config from @shellui/config (injected at build time) and user preference from settings.
|
|
33
27
|
* Exported so the settings UI can re-initialize when the user re-enables reporting.
|
|
34
28
|
*/
|
|
35
29
|
export function initSentry(): void {
|
|
@@ -40,16 +34,32 @@ export function initSentry(): void {
|
|
|
40
34
|
if (!isErrorReportingEnabled()) {
|
|
41
35
|
return;
|
|
42
36
|
}
|
|
43
|
-
const
|
|
44
|
-
const dsn =
|
|
37
|
+
const sentry = shelluiConfig?.sentry;
|
|
38
|
+
const dsn = sentry?.dsn;
|
|
45
39
|
if (!dsn || typeof dsn !== 'string') {
|
|
46
40
|
return;
|
|
47
41
|
}
|
|
48
42
|
void import('@sentry/react').then((Sentry) => {
|
|
43
|
+
const isLocalhost =
|
|
44
|
+
typeof window !== 'undefined' &&
|
|
45
|
+
(window.location.hostname === 'localhost' ||
|
|
46
|
+
window.location.hostname === '127.0.0.1' ||
|
|
47
|
+
window.location.hostname === '[::1]');
|
|
48
|
+
|
|
49
|
+
// For localhost, use tunnel through the dev server to avoid CORS issues
|
|
50
|
+
// The tunnel endpoint proxies requests to Sentry, bypassing browser CORS restrictions
|
|
51
|
+
const tunnel =
|
|
52
|
+
isLocalhost && typeof window !== 'undefined'
|
|
53
|
+
? `${window.location.origin}/api/sentry-tunnel`
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
49
56
|
Sentry.init({
|
|
50
57
|
dsn,
|
|
51
|
-
environment:
|
|
52
|
-
release:
|
|
58
|
+
environment: sentry.environment ?? 'production',
|
|
59
|
+
release: sentry.release,
|
|
60
|
+
sendDefaultPii: true,
|
|
61
|
+
// Use tunnel for localhost to avoid CORS issues
|
|
62
|
+
...(tunnel && { tunnel }),
|
|
53
63
|
});
|
|
54
64
|
sentryLoaded = true;
|
|
55
65
|
});
|
|
@@ -69,4 +79,64 @@ export function closeSentry(): void {
|
|
|
69
79
|
});
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Ensure Sentry is initialized and ready, then capture an exception.
|
|
84
|
+
* Useful for manually triggering error reports (e.g., test button).
|
|
85
|
+
* This function will initialize Sentry even in dev mode if needed for testing.
|
|
86
|
+
* @param error - The error to capture
|
|
87
|
+
* @returns Promise that resolves when the error has been sent (or rejected if Sentry is unavailable)
|
|
88
|
+
*/
|
|
89
|
+
export async function captureException(error: Error): Promise<void> {
|
|
90
|
+
const sentry = shelluiConfig?.sentry;
|
|
91
|
+
const dsn = sentry?.dsn;
|
|
92
|
+
|
|
93
|
+
if (!dsn || typeof dsn !== 'string') {
|
|
94
|
+
throw new Error('Sentry DSN not configured');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If Sentry is not loaded, initialize it (even in dev mode for testing)
|
|
98
|
+
if (!sentryLoaded) {
|
|
99
|
+
// Check if error reporting is enabled
|
|
100
|
+
if (!isErrorReportingEnabled()) {
|
|
101
|
+
throw new Error('Error reporting is disabled in settings');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Import and initialize Sentry (bypass dev mode check for manual testing)
|
|
105
|
+
const Sentry = await import('@sentry/react');
|
|
106
|
+
const isLocalhost =
|
|
107
|
+
typeof window !== 'undefined' &&
|
|
108
|
+
(window.location.hostname === 'localhost' ||
|
|
109
|
+
window.location.hostname === '127.0.0.1' ||
|
|
110
|
+
window.location.hostname === '[::1]');
|
|
111
|
+
|
|
112
|
+
// For localhost, use tunnel through the dev server to avoid CORS issues
|
|
113
|
+
// The tunnel endpoint proxies requests to Sentry, bypassing browser CORS restrictions
|
|
114
|
+
const tunnel =
|
|
115
|
+
isLocalhost && typeof window !== 'undefined'
|
|
116
|
+
? `${window.location.origin}/api/sentry-tunnel`
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
Sentry.init({
|
|
120
|
+
dsn,
|
|
121
|
+
environment: sentry.environment ?? 'production',
|
|
122
|
+
release: sentry.release,
|
|
123
|
+
sendDefaultPii: true,
|
|
124
|
+
// Use tunnel for localhost to avoid CORS issues
|
|
125
|
+
...(tunnel && { tunnel }),
|
|
126
|
+
});
|
|
127
|
+
sentryLoaded = true;
|
|
128
|
+
|
|
129
|
+
// Wait a bit for Sentry to fully initialize
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Capture the exception
|
|
134
|
+
const Sentry = await import('@sentry/react');
|
|
135
|
+
if (Sentry.captureException) {
|
|
136
|
+
Sentry.captureException(error);
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error('Sentry captureException not available');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
72
142
|
initSentry();
|
|
@@ -10,6 +10,7 @@ import { SettingsContext } from './SettingsContext';
|
|
|
10
10
|
import { useConfig } from '../config/useConfig';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
12
|
import type { NavigationItem, NavigationGroup } from '../config/types';
|
|
13
|
+
import { getEffectiveUrl } from '../layouts/utils';
|
|
13
14
|
|
|
14
15
|
const logger = getLogger('shellcore');
|
|
15
16
|
|
|
@@ -39,7 +40,7 @@ function buildSettingsWithNavigation(
|
|
|
39
40
|
if (!navigation?.length) return settings;
|
|
40
41
|
const items: SettingsNavigationItem[] = flattenNavigationItems(navigation).map((item) => ({
|
|
41
42
|
path: item.path,
|
|
42
|
-
url: item
|
|
43
|
+
url: getEffectiveUrl(item),
|
|
43
44
|
label: resolveLabel(item.label, lang),
|
|
44
45
|
}));
|
|
45
46
|
return { ...settings, navigation: { items } };
|
|
@@ -26,13 +26,17 @@ import { useConfig } from '../config/useConfig';
|
|
|
26
26
|
import { isTauri } from '../../service-worker/register';
|
|
27
27
|
import { Button } from '../../components/ui/button';
|
|
28
28
|
import { ChevronRightIcon, ChevronLeftIcon } from './SettingsIcons';
|
|
29
|
+
import { flattenNavigationItems, resolveLocalizedString } from '../layouts/utils';
|
|
30
|
+
import { ApplicationSettingsPanel } from './components/ApplicationSettingsPanel';
|
|
31
|
+
import type { NavigationItem } from '../config/types';
|
|
32
|
+
import { cn } from '../../lib/utils';
|
|
29
33
|
|
|
30
34
|
export const SettingsView = () => {
|
|
31
35
|
const location = useLocation();
|
|
32
36
|
const navigate = useNavigate();
|
|
33
37
|
const { settings } = useSettings();
|
|
34
38
|
const { config } = useConfig();
|
|
35
|
-
const { t } = useTranslation('settings');
|
|
39
|
+
const { t, i18n } = useTranslation('settings');
|
|
36
40
|
// Re-check isTauri after mount and after a short delay so we catch late-injected __TAURI__ in dev
|
|
37
41
|
const [isTauriEnv, setIsTauriEnv] = useState(() => isTauri());
|
|
38
42
|
|
|
@@ -70,10 +74,46 @@ export const SettingsView = () => {
|
|
|
70
74
|
);
|
|
71
75
|
}, [settings.developerFeatures.enabled, routesWithoutTauriSw]);
|
|
72
76
|
|
|
77
|
+
// Application settings from navigation items with settings URL
|
|
78
|
+
const applicationRoutes = useMemo(() => {
|
|
79
|
+
const lang = i18n.language || 'en';
|
|
80
|
+
const flat = config?.navigation ? flattenNavigationItems(config.navigation) : [];
|
|
81
|
+
return flat
|
|
82
|
+
.filter((item): item is NavigationItem & { settings: string } => Boolean(item.settings))
|
|
83
|
+
.map((item) => {
|
|
84
|
+
const pathPrefix = `${urls.settings.replace(/^\/+/, '')}/app-${item.path}`;
|
|
85
|
+
const navItem: NavigationItem = {
|
|
86
|
+
...item,
|
|
87
|
+
url: item.settings,
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
name: resolveLocalizedString(item.label, lang),
|
|
91
|
+
iconSrc: item.icon ?? undefined,
|
|
92
|
+
path: `app-${item.path}`,
|
|
93
|
+
element: (
|
|
94
|
+
<ApplicationSettingsPanel
|
|
95
|
+
url={item.settings}
|
|
96
|
+
pathPrefix={pathPrefix}
|
|
97
|
+
navItem={navItem}
|
|
98
|
+
/>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}, [config?.navigation, i18n.language]);
|
|
103
|
+
|
|
104
|
+
// All routes (core + applications) for selection and routing
|
|
105
|
+
const allRoutes = useMemo(
|
|
106
|
+
() => [...filteredRoutes, ...applicationRoutes],
|
|
107
|
+
[filteredRoutes, applicationRoutes],
|
|
108
|
+
);
|
|
109
|
+
|
|
73
110
|
// Group routes by category
|
|
74
111
|
const groupedRoutes = useMemo(() => {
|
|
75
112
|
const developerOnlyPaths = ['developpers', 'service-worker'];
|
|
76
113
|
const groups = [
|
|
114
|
+
...(applicationRoutes.length > 0
|
|
115
|
+
? [{ title: t('categories.applications'), routes: applicationRoutes }]
|
|
116
|
+
: []),
|
|
77
117
|
{
|
|
78
118
|
title: t('categories.preferences'),
|
|
79
119
|
routes: filteredRoutes.filter((route) =>
|
|
@@ -90,7 +130,7 @@ export const SettingsView = () => {
|
|
|
90
130
|
},
|
|
91
131
|
];
|
|
92
132
|
return groups.filter((group) => group.routes.length > 0);
|
|
93
|
-
}, [filteredRoutes, t]);
|
|
133
|
+
}, [filteredRoutes, applicationRoutes, t]);
|
|
94
134
|
|
|
95
135
|
// Find matching nav item by checking if URL contains or ends with the item path
|
|
96
136
|
const getSelectedItemFromUrl = useCallback(() => {
|
|
@@ -98,7 +138,7 @@ export const SettingsView = () => {
|
|
|
98
138
|
|
|
99
139
|
// Find matching nav item by checking if pathname contains the item path
|
|
100
140
|
// This works regardless of the URL structure/prefix
|
|
101
|
-
const matchedItem =
|
|
141
|
+
const matchedItem = allRoutes.find((item) => {
|
|
102
142
|
// Normalize paths for comparison (remove leading/trailing slashes)
|
|
103
143
|
const normalizedPathname = pathname.replace(/^\/+|\/+$/g, '');
|
|
104
144
|
const normalizedItemPath = item.path.replace(/^\/+|\/+$/g, '');
|
|
@@ -112,7 +152,7 @@ export const SettingsView = () => {
|
|
|
112
152
|
});
|
|
113
153
|
|
|
114
154
|
return matchedItem;
|
|
115
|
-
}, [location.pathname,
|
|
155
|
+
}, [location.pathname, allRoutes]);
|
|
116
156
|
|
|
117
157
|
const selectedItem = useMemo(() => getSelectedItemFromUrl(), [getSelectedItemFromUrl]);
|
|
118
158
|
|
|
@@ -139,7 +179,7 @@ export const SettingsView = () => {
|
|
|
139
179
|
|
|
140
180
|
// Pathname doesn't match settings path structure - not in settings
|
|
141
181
|
return false;
|
|
142
|
-
}, [location.pathname
|
|
182
|
+
}, [location.pathname]);
|
|
143
183
|
|
|
144
184
|
// Navigate back to settings root
|
|
145
185
|
const handleBackToSettings = useCallback(() => {
|
|
@@ -168,7 +208,17 @@ export const SettingsView = () => {
|
|
|
168
208
|
onClick={() => navigate(`${urls.settings}/${item.path}`)}
|
|
169
209
|
className="cursor-pointer"
|
|
170
210
|
>
|
|
171
|
-
|
|
211
|
+
{'icon' in item && item.icon ? (
|
|
212
|
+
<item.icon />
|
|
213
|
+
) : 'iconSrc' in item && item.iconSrc ? (
|
|
214
|
+
<img
|
|
215
|
+
src={item.iconSrc}
|
|
216
|
+
alt=""
|
|
217
|
+
className="h-4 w-4 shrink-0"
|
|
218
|
+
/>
|
|
219
|
+
) : (
|
|
220
|
+
<span className="h-4 w-4 shrink-0" />
|
|
221
|
+
)}
|
|
172
222
|
<span>{item.name}</span>
|
|
173
223
|
</button>
|
|
174
224
|
</SidebarMenuButton>
|
|
@@ -203,8 +253,19 @@ export const SettingsView = () => {
|
|
|
203
253
|
</h2>
|
|
204
254
|
<div className="flex flex-col bg-card rounded-lg overflow-hidden border border-border">
|
|
205
255
|
{group.routes.map((item, itemIndex) => {
|
|
206
|
-
const Icon = item.icon;
|
|
207
256
|
const isLast = itemIndex === group.routes.length - 1;
|
|
257
|
+
const iconEl =
|
|
258
|
+
'icon' in item && item.icon ? (
|
|
259
|
+
<item.icon />
|
|
260
|
+
) : 'iconSrc' in item && item.iconSrc ? (
|
|
261
|
+
<img
|
|
262
|
+
src={item.iconSrc}
|
|
263
|
+
alt=""
|
|
264
|
+
className="h-4 w-4 shrink-0"
|
|
265
|
+
/>
|
|
266
|
+
) : (
|
|
267
|
+
<span className="h-4 w-4 shrink-0" />
|
|
268
|
+
);
|
|
208
269
|
return (
|
|
209
270
|
<div
|
|
210
271
|
key={item.name}
|
|
@@ -218,9 +279,7 @@ export const SettingsView = () => {
|
|
|
218
279
|
className="w-full flex items-center justify-between px-4 py-3 bg-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground transition-colors cursor-pointer rounded-none"
|
|
219
280
|
>
|
|
220
281
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
221
|
-
<div className="flex-shrink-0 text-foreground/70">
|
|
222
|
-
<Icon />
|
|
223
|
-
</div>
|
|
282
|
+
<div className="flex-shrink-0 text-foreground/70">{iconEl}</div>
|
|
224
283
|
<span className="text-sm font-normal text-foreground">
|
|
225
284
|
{item.name}
|
|
226
285
|
</span>
|
|
@@ -256,15 +315,15 @@ export const SettingsView = () => {
|
|
|
256
315
|
<Route
|
|
257
316
|
index
|
|
258
317
|
element={
|
|
259
|
-
|
|
318
|
+
allRoutes.length > 0 ? (
|
|
260
319
|
<Navigate
|
|
261
|
-
to={`${urls.settings}/${
|
|
320
|
+
to={`${urls.settings}/${allRoutes[0].path}`}
|
|
262
321
|
replace
|
|
263
322
|
/>
|
|
264
323
|
) : null
|
|
265
324
|
}
|
|
266
325
|
/>
|
|
267
|
-
{
|
|
326
|
+
{allRoutes.map((item) => (
|
|
268
327
|
<Route
|
|
269
328
|
key={item.path}
|
|
270
329
|
path={item.path}
|
|
@@ -294,7 +353,12 @@ export const SettingsView = () => {
|
|
|
294
353
|
</div>
|
|
295
354
|
</header>
|
|
296
355
|
)}
|
|
297
|
-
<div
|
|
356
|
+
<div
|
|
357
|
+
className={cn(
|
|
358
|
+
'flex flex-1 flex-col gap-4 overflow-y-auto',
|
|
359
|
+
!selectedItem?.path?.startsWith('app-') && 'p-4 pt-0',
|
|
360
|
+
)}
|
|
361
|
+
>
|
|
298
362
|
<Routes>
|
|
299
363
|
<Route
|
|
300
364
|
index
|
|
@@ -311,7 +375,7 @@ export const SettingsView = () => {
|
|
|
311
375
|
</div>
|
|
312
376
|
}
|
|
313
377
|
/>
|
|
314
|
-
{
|
|
378
|
+
{allRoutes.map((item) => (
|
|
315
379
|
<Route
|
|
316
380
|
key={item.path}
|
|
317
381
|
path={item.path}
|
|
@@ -5,14 +5,26 @@ import { closeSentry, initSentry } from '../../sentry/initSentry';
|
|
|
5
5
|
import { useSettings } from '../hooks/useSettings';
|
|
6
6
|
import { Button } from '../../../components/ui/button';
|
|
7
7
|
import { shellui } from '@shellui/sdk';
|
|
8
|
+
import { useCookieConsent } from '../../cookieConsent/useCookieConsent';
|
|
8
9
|
|
|
9
10
|
export const Advanced = () => {
|
|
10
11
|
const { t } = useTranslation('settings');
|
|
11
12
|
const { config } = useConfig();
|
|
12
13
|
const { settings, updateSetting, resetAllData } = useSettings();
|
|
13
14
|
const errorReportingConfigured = Boolean(config?.sentry?.dsn);
|
|
15
|
+
const { isAccepted: sentryConsentAccepted } = useCookieConsent('sentry.io');
|
|
14
16
|
|
|
15
17
|
const handleErrorReportingChange = (checked: boolean) => {
|
|
18
|
+
// Don't allow enabling if cookie consent hasn't been approved
|
|
19
|
+
if (checked && !sentryConsentAccepted) {
|
|
20
|
+
shellui.toast({
|
|
21
|
+
title: 'Cookie consent required',
|
|
22
|
+
description:
|
|
23
|
+
'Please approve Sentry cookie consent in Data Privacy settings to enable error reporting.',
|
|
24
|
+
type: 'error',
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
16
28
|
updateSetting('errorReporting', { enabled: checked });
|
|
17
29
|
if (checked) {
|
|
18
30
|
initSentry();
|
|
@@ -56,15 +68,18 @@ export const Advanced = () => {
|
|
|
56
68
|
</span>
|
|
57
69
|
<p className="text-sm text-muted-foreground">
|
|
58
70
|
{errorReportingConfigured
|
|
59
|
-
?
|
|
71
|
+
? sentryConsentAccepted
|
|
72
|
+
? t('advanced.errorReporting.statusConfigured')
|
|
73
|
+
: 'Cookie consent required to enable error reporting'
|
|
60
74
|
: t('advanced.errorReporting.statusNotConfigured')}
|
|
61
75
|
</p>
|
|
62
76
|
</div>
|
|
63
77
|
{errorReportingConfigured && (
|
|
64
78
|
<Switch
|
|
65
79
|
id="error-reporting"
|
|
66
|
-
checked={settings.errorReporting.enabled}
|
|
80
|
+
checked={sentryConsentAccepted && settings.errorReporting.enabled}
|
|
67
81
|
onCheckedChange={handleErrorReportingChange}
|
|
82
|
+
disabled={!sentryConsentAccepted}
|
|
68
83
|
/>
|
|
69
84
|
)}
|
|
70
85
|
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { NavigationItem } from '../../config/types';
|
|
2
|
+
import { ContentView } from '../../../components/ContentView';
|
|
3
|
+
|
|
4
|
+
interface ApplicationSettingsPanelProps {
|
|
5
|
+
url: string;
|
|
6
|
+
pathPrefix: string;
|
|
7
|
+
navItem: NavigationItem;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ApplicationSettingsPanel = ({
|
|
11
|
+
url,
|
|
12
|
+
pathPrefix,
|
|
13
|
+
navItem,
|
|
14
|
+
}: ApplicationSettingsPanelProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<div className="m-0 p-0 flex flex-1 flex-col min-h-0 w-full overflow-hidden bg-background">
|
|
17
|
+
<ContentView
|
|
18
|
+
url={url}
|
|
19
|
+
pathPrefix={pathPrefix}
|
|
20
|
+
navItem={navItem}
|
|
21
|
+
ignoreMessages={true}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -2,7 +2,11 @@ import { useTranslation } from 'react-i18next';
|
|
|
2
2
|
import { shellui } from '@shellui/sdk';
|
|
3
3
|
import { useSettings } from '../hooks/useSettings';
|
|
4
4
|
import { useConfig } from '../../config/useConfig';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
flattenNavigationItems,
|
|
7
|
+
getNavPathPrefix,
|
|
8
|
+
resolveLocalizedString,
|
|
9
|
+
} from '../../layouts/utils';
|
|
6
10
|
import { Switch } from '../../../components/ui/switch';
|
|
7
11
|
import { Select } from '../../../components/ui/select';
|
|
8
12
|
import { Button } from '../../../components/ui/button';
|
|
@@ -10,6 +14,8 @@ import { ToastTestButtons } from './develop/ToastTestButtons';
|
|
|
10
14
|
import { DialogTestButtons } from './develop/DialogTestButtons';
|
|
11
15
|
import { ModalTestButtons } from './develop/ModalTestButtons';
|
|
12
16
|
import { DrawerTestButtons } from './develop/DrawerTestButtons';
|
|
17
|
+
import { captureException } from '../../sentry/initSentry';
|
|
18
|
+
import { useCookieConsent } from '../../cookieConsent/useCookieConsent';
|
|
13
19
|
import type { LayoutType } from '../../config/types';
|
|
14
20
|
|
|
15
21
|
export const Develop = () => {
|
|
@@ -23,6 +29,8 @@ export const Develop = () => {
|
|
|
23
29
|
(item, index, self) => index === self.findIndex((i) => i.path === item.path),
|
|
24
30
|
)
|
|
25
31
|
: [];
|
|
32
|
+
const errorReportingConfigured = Boolean(config?.sentry?.dsn);
|
|
33
|
+
const { isAccepted: sentryConsentAccepted } = useCookieConsent('sentry.io');
|
|
26
34
|
|
|
27
35
|
return (
|
|
28
36
|
<div className="space-y-6">
|
|
@@ -120,8 +128,8 @@ export const Develop = () => {
|
|
|
120
128
|
<option value="">{t('develop.navigation.placeholder')}</option>
|
|
121
129
|
{navItems.map((item) => (
|
|
122
130
|
<option
|
|
123
|
-
key={item.path}
|
|
124
|
-
value={
|
|
131
|
+
key={item.path || 'root'}
|
|
132
|
+
value={getNavPathPrefix(item)}
|
|
125
133
|
>
|
|
126
134
|
{resolveLocalizedString(item.label, currentLanguage) || item.path}
|
|
127
135
|
</option>
|
|
@@ -142,7 +150,7 @@ export const Develop = () => {
|
|
|
142
150
|
{t('develop.layout.title')}
|
|
143
151
|
</h3>
|
|
144
152
|
<div className="flex flex-wrap gap-2">
|
|
145
|
-
{(['sidebar', 'windows'] as const).map((layoutMode) => (
|
|
153
|
+
{(['sidebar', 'app-bar', 'windows'] as const).map((layoutMode) => (
|
|
146
154
|
<Button
|
|
147
155
|
key={layoutMode}
|
|
148
156
|
variant={effectiveLayout === layoutMode ? 'default' : 'outline'}
|
|
@@ -167,6 +175,62 @@ export const Develop = () => {
|
|
|
167
175
|
<DialogTestButtons />
|
|
168
176
|
<ModalTestButtons />
|
|
169
177
|
<DrawerTestButtons />
|
|
178
|
+
{errorReportingConfigured && (
|
|
179
|
+
<div className="flex items-center justify-between">
|
|
180
|
+
<div className="space-y-0.5">
|
|
181
|
+
<span
|
|
182
|
+
className="text-sm font-medium leading-none"
|
|
183
|
+
style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
|
|
184
|
+
>
|
|
185
|
+
Test Error Reporting
|
|
186
|
+
</span>
|
|
187
|
+
<p className="text-sm text-muted-foreground">
|
|
188
|
+
Trigger a test error to verify Sentry is working correctly
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
<Button
|
|
192
|
+
variant="destructive"
|
|
193
|
+
onClick={async () => {
|
|
194
|
+
if (!sentryConsentAccepted) {
|
|
195
|
+
shellui.toast({
|
|
196
|
+
title: 'Cookie consent required',
|
|
197
|
+
description:
|
|
198
|
+
'Please approve Sentry cookie consent in Data Privacy settings to test error reporting.',
|
|
199
|
+
type: 'error',
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const testError = new Error('This is your first error!');
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Ensure Sentry is initialized and capture the error
|
|
208
|
+
await captureException(testError);
|
|
209
|
+
|
|
210
|
+
// Show success toast
|
|
211
|
+
shellui.toast({
|
|
212
|
+
title: 'Test error triggered',
|
|
213
|
+
description: 'The error has been sent to Sentry for tracking.',
|
|
214
|
+
type: 'success',
|
|
215
|
+
});
|
|
216
|
+
} catch (err) {
|
|
217
|
+
// If Sentry capture failed, show error toast
|
|
218
|
+
shellui.toast({
|
|
219
|
+
title: 'Failed to send error to Sentry',
|
|
220
|
+
description:
|
|
221
|
+
err instanceof Error
|
|
222
|
+
? err.message
|
|
223
|
+
: 'Sentry is not configured or not available.',
|
|
224
|
+
type: 'error',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}}
|
|
228
|
+
disabled={!sentryConsentAccepted}
|
|
229
|
+
>
|
|
230
|
+
Break the world
|
|
231
|
+
</Button>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
170
234
|
</div>
|
|
171
235
|
</div>
|
|
172
236
|
</div>
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
"settings": "Settings",
|
|
3
3
|
"welcome": "Welcome to {{title}}",
|
|
4
4
|
"getStarted": "Select a navigation item to get started.",
|
|
5
|
+
"homeConfig": {
|
|
6
|
+
"intro": "To show custom content on the home page:",
|
|
7
|
+
"startUrl": "Set start_url in shellui.config to redirect \"/\" to another path (e.g. start_url: '/playground').",
|
|
8
|
+
"rootNav": "Or add a navigation item with path \"\" or \"/\" to display that item at \"/\"."
|
|
9
|
+
},
|
|
5
10
|
"navigationError": "Navigation error",
|
|
6
11
|
"navigationNotAllowed": "This URL is not configured in the app navigation.",
|
|
7
12
|
"errorBoundary": {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"title": "Settings",
|
|
3
3
|
"categories": {
|
|
4
4
|
"preferences": "Preferences",
|
|
5
|
+
"applications": "Applications",
|
|
5
6
|
"system": "System",
|
|
6
7
|
"developer": "Developer"
|
|
7
8
|
},
|
|
@@ -199,7 +200,8 @@
|
|
|
199
200
|
"title": "Layout",
|
|
200
201
|
"description": "Switch between sidebar layout and windows (taskbar) layout. This overrides the app config for this session.",
|
|
201
202
|
"sidebar": "Sidebar",
|
|
202
|
-
"windows": "Windows"
|
|
203
|
+
"windows": "Windows (experimental)",
|
|
204
|
+
"app-bar": "App bar"
|
|
203
205
|
},
|
|
204
206
|
"logging": {
|
|
205
207
|
"title": "Logging",
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
"settings": "Paramètres",
|
|
3
3
|
"welcome": "Bienvenue sur {{title}}",
|
|
4
4
|
"getStarted": "Sélectionnez un élément de navigation pour commencer.",
|
|
5
|
+
"homeConfig": {
|
|
6
|
+
"intro": "Pour afficher du contenu personnalisé sur la page d'accueil :",
|
|
7
|
+
"startUrl": "Définissez start_url dans shellui.config pour rediriger \"/\" vers un autre chemin (ex. start_url: '/playground').",
|
|
8
|
+
"rootNav": "Ou ajoutez un élément de navigation avec path \"\" ou \"/\" pour l'afficher à \"/\"."
|
|
9
|
+
},
|
|
5
10
|
"navigationError": "Erreur de navigation",
|
|
6
11
|
"navigationNotAllowed": "Cette URL n'est pas configurée dans la navigation de l'application.",
|
|
7
12
|
"errorBoundary": {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"title": "Paramètres",
|
|
3
3
|
"categories": {
|
|
4
4
|
"preferences": "Préférences",
|
|
5
|
+
"applications": "Applications",
|
|
5
6
|
"system": "Système",
|
|
6
7
|
"developer": "Développeur"
|
|
7
8
|
},
|
|
@@ -199,7 +200,8 @@
|
|
|
199
200
|
"title": "Disposition",
|
|
200
201
|
"description": "Basculer entre la disposition avec barre latérale et la disposition fenêtres (barre des tâches). Remplace la config de l'app pour cette session.",
|
|
201
202
|
"sidebar": "Barre latérale",
|
|
202
|
-
"windows": "Fenêtres"
|
|
203
|
+
"windows": "Fenêtres (expérimental)",
|
|
204
|
+
"app-bar": "Barre d'app"
|
|
203
205
|
},
|
|
204
206
|
"logging": {
|
|
205
207
|
"title": "Journalisation",
|
package/src/index.css
CHANGED
|
@@ -318,6 +318,16 @@
|
|
|
318
318
|
filter: brightness(0) invert(1) saturate(100%);
|
|
319
319
|
opacity: 0.9;
|
|
320
320
|
}
|
|
321
|
+
|
|
322
|
+
/* App bar layout logo - same as sidebar: dark in light mode, white in dark mode */
|
|
323
|
+
[data-layout='app-bar'] img.app-bar-logo {
|
|
324
|
+
filter: brightness(0) saturate(100%);
|
|
325
|
+
opacity: 0.9;
|
|
326
|
+
}
|
|
327
|
+
.dark [data-layout='app-bar'] img.app-bar-logo {
|
|
328
|
+
filter: brightness(0) invert(1) saturate(100%);
|
|
329
|
+
opacity: 0.9;
|
|
330
|
+
}
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
/* Dialog / modal overlay and content animations */
|
package/src/lib/z-index.ts
CHANGED
|
@@ -9,6 +9,8 @@ export const Z_INDEX = {
|
|
|
9
9
|
/** Modal overlay and content (settings panel, etc.) */
|
|
10
10
|
MODAL_OVERLAY: 10000,
|
|
11
11
|
MODAL_CONTENT: 10001,
|
|
12
|
+
/** Tooltip content (above modals so tooltips show over dialogs if needed) */
|
|
13
|
+
TOOLTIP: 10002,
|
|
12
14
|
/** Drawer overlay and content (slide-out panels; same level as modal) */
|
|
13
15
|
DRAWER_OVERLAY: 10000,
|
|
14
16
|
DRAWER_CONTENT: 10001,
|