Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.

21 changes: 21 additions & 0 deletions src/components/ThemeManager/ThemeManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import { resolveTheme } from './ThemeManager.tsx';

describe('ThemeManager', () => {
describe('resolveTheme()', () => {
it('returns theme coming from URL when it is truthy', () => {
expect(resolveTheme('sap_fiori_3', true)).toBe('sap_fiori_3');
expect(resolveTheme('custom_theme', false)).toBe('custom_theme');
});

it('falls back to dark default when URL theme is falsy and user prefers dark mode', () => {
expect(resolveTheme(null, true)).toBe('sap_horizon_dark');
expect(resolveTheme('', true)).toBe('sap_horizon_dark');
});

it('falls back to light default when URL theme is falsy and user prefers light mode', () => {
expect(resolveTheme(null, false)).toBe('sap_horizon');
expect(resolveTheme('', false)).toBe('sap_horizon');
});
});
});
25 changes: 25 additions & 0 deletions src/components/ThemeManager/ThemeManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js';
import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts';

const DEFAULT_THEME_LIGHT = 'sap_horizon';
const DEFAULT_THEME_DARK = 'sap_horizon_dark';

export function resolveTheme(themeFromUrl: string | null, isDarkModePreferred: boolean): string {
if (themeFromUrl) {
return themeFromUrl;
}
return isDarkModePreferred ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT;
}

export function ThemeManager() {
const isDarkModePreferred = useIsDarkModePreferred();
const themeFromUrl = new URL(window.location.href).searchParams.get('sap-theme');

useEffect(() => {
const resolvedTheme = resolveTheme(themeFromUrl, isDarkModePreferred);
void setTheme(resolvedTheme);
}, [isDarkModePreferred, themeFromUrl]);

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 { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts';
type YamlViewerProps = { yamlString: string; filename: string };
const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {
const toast = useToast();
const { t } = useTranslation();
const { isDarkMode } = useThemeMode();
const isDarkModePreferred = useIsDarkModePreferred();
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={isDarkModePreferred ? materialDark : materialLight}
showLineNumbers
lineNumberStyle={{
paddingRight: '20px',
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useIsDarkModePreferred.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useSyncExternalStore } from 'react';

function makeMediaQueryStore(mediaQuery: string) {
function getSnapshot() {
return window.matchMedia(mediaQuery).matches;
}

function subscribe(callback: () => void) {
const mediaQueryList = window.matchMedia(mediaQuery);
mediaQueryList.addEventListener('change', callback);

return () => {
mediaQueryList.removeEventListener('change', callback);
};
}

return function useMediaQuery() {
return useSyncExternalStore(subscribe, getSnapshot);
};
}

export const useIsDarkModePreferred = makeMediaQueryStore('(prefers-color-scheme: dark)');
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/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