Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions src/components/Core/DarkModeSystemSwitcher.tsx

This file was deleted.

13 changes: 13 additions & 0 deletions src/components/ThemeManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect } from 'react';
import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js';
import { useTheme } from '../hooks/useTheme.ts';

export function ThemeManager() {
const { theme } = useTheme();

useEffect(() => {
void setTheme(theme);
}, [theme]);

return null;
}
19 changes: 5 additions & 14 deletions src/components/Yaml/YamlViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { FC } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
materialLight,
materialDark,
} from 'react-syntax-highlighter/dist/esm/styles/prism';
import { materialLight, materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism';

import { Button, FlexBox } from '@ui5/webcomponents-react';
import styles from './YamlViewer.module.css';
import { useToast } from '../../context/ToastContext.tsx';
import { useTranslation } from 'react-i18next';
import { useThemeMode } from '../../lib/useThemeMode.ts';
import { useTheme } from '../../hooks/useTheme.ts';
type YamlViewerProps = { yamlString: string; filename: string };
const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {
const toast = useToast();
const { t } = useTranslation();
const { isDarkMode } = useThemeMode();
const { isDarkTheme } = useTheme();
const copyToClipboard = () => {
navigator.clipboard.writeText(yamlString);
toast.show(t('yaml.copiedToClipboard'));
Expand All @@ -33,13 +30,7 @@ const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {

return (
<div className={styles.container}>
<FlexBox
className={styles.buttons}
direction="Row"
justifyContent="End"
alignItems="Baseline"
gap={16}
>
<FlexBox className={styles.buttons} direction="Row" justifyContent="End" alignItems="Baseline" gap={16}>
<Button icon="copy" onClick={copyToClipboard}>
{t('buttons.copy')}
</Button>
Expand All @@ -49,7 +40,7 @@ const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {
</FlexBox>
<SyntaxHighlighter
language="yaml"
style={isDarkMode ? materialDark : materialLight}
style={isDarkTheme ? materialDark : materialLight}
showLineNumbers
lineNumberStyle={{
paddingRight: '20px',
Expand Down
38 changes: 38 additions & 0 deletions src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useSyncExternalStore } from 'react';
import { Theme } from '@ui5/webcomponents-react';

const DEFAULT_THEME_LIGHT = Theme.sap_horizon;
const DEFAULT_THEME_DARK = Theme.sap_horizon_dark;
const DARK_SAP_THEMES = new Set<string>([
Theme.sap_fiori_3_dark,
Theme.sap_fiori_3_hcb,
Theme.sap_horizon_dark,
Theme.sap_horizon_hcb,
]);

function useSystemDarkModePreference() {
return useSyncExternalStore(
(callback) => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', callback);
return () => mediaQuery.removeEventListener('change', callback);
},
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
);
}

export function useTheme() {
const systemPrefersDark = useSystemDarkModePreference();
const themeFromUrl = new URL(window.location.href).searchParams.get('sap-theme');

// Theme from URL takes precedence over system settings
const theme = themeFromUrl || (systemPrefersDark ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT);

// For well-defined SAP themes, return if they are light or dark – unknown themes will fall back to light
const isDarkTheme = DARK_SAP_THEMES.has(theme);

return {
theme,
isDarkTheme,
};
}
7 changes: 0 additions & 7 deletions src/lib/useThemeMode.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ToastProvider } from './context/ToastContext.tsx';
import { CopyButtonProvider } from './context/CopyButtonContext.tsx';
import { FrontendConfigProvider } from './context/FrontendConfigContext.tsx';
import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes
import { DarkModeSystemSwitcher } from './components/Core/DarkModeSystemSwitcher.tsx';
import { ThemeManager } from './components/ThemeManager.tsx';
import '.././i18n.ts';
import './utils/i18n/timeAgo';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
Expand Down Expand Up @@ -49,7 +49,7 @@ export function createApp() {
<ApolloClientProvider>
<App />
</ApolloClientProvider>
<DarkModeSystemSwitcher />
<ThemeManager />
</SWRConfig>
</CopyButtonProvider>
</ToastProvider>
Expand Down
36 changes: 11 additions & 25 deletions src/spaces/onboarding/auth/AuthContextOnboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ interface AuthContextOnboardingType {
logout: () => Promise<void>;
}

const AuthContextOnboarding = createContext<AuthContextOnboardingType | null>(
null,
);
const AuthContextOnboarding = createContext<AuthContextOnboardingType | null>(null);

export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
Expand All @@ -40,22 +38,16 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
} catch (_) {
/* safe to ignore */
}
throw new Error(
errorBody?.message ||
`Authentication check failed with status: ${response.status}`,
);
throw new Error(errorBody?.message || `Authentication check failed with status: ${response.status}`);
}

const body = await response.json();
const validationResult = MeResponseSchema.safeParse(body);
if (!validationResult.success) {
throw new Error(
`Auth API response validation failed: ${validationResult.error.flatten()}`,
);
throw new Error(`Auth API response validation failed: ${validationResult.error.flatten()}`);
}

const { isAuthenticated: apiIsAuthenticated, user: apiUser } =
validationResult.data;
const { isAuthenticated: apiIsAuthenticated, user: apiUser } = validationResult.data;
setUser(apiUser);
setIsAuthenticated(apiIsAuthenticated);

Expand All @@ -75,10 +67,10 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {

const login = () => {
sessionStorage.setItem(AUTH_FLOW_SESSION_KEY, 'onboarding');

window.location.replace(
`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(window.location.hash)}`,
);
// The query parameters and hash fragments need to be preserved, e.g. /?sap-theme=sap_horizon#/mcp/projects
const { search, hash } = window.location;
const redirectTo = (search ? `/${search}` : '') + hash;
window.location.replace(`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(redirectTo)}`);
};

const logout = async () => {
Expand All @@ -94,9 +86,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
} catch (_) {
/* safe to ignore */
}
throw new Error(
errorBody?.message || `Logout failed with status: ${response.status}`,
);
throw new Error(errorBody?.message || `Logout failed with status: ${response.status}`);
}

await refreshAuthStatus();
Expand All @@ -106,9 +96,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
};

return (
<AuthContextOnboarding
value={{ isLoading, isAuthenticated, user, error, login, logout }}
>
<AuthContextOnboarding value={{ isLoading, isAuthenticated, user, error, login, logout }}>
{children}
</AuthContextOnboarding>
);
Expand All @@ -117,9 +105,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
export const useAuthOnboarding = () => {
const context = use(AuthContextOnboarding);
if (!context) {
throw new Error(
'useAuthOnboarding must be used within an AuthProviderOnboarding.',
);
throw new Error('useAuthOnboarding must be used within an AuthProviderOnboarding.');
}
return context;
};
Loading