mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
fix(console): fix SSO connector logo fallback logic (#4990)
This commit is contained in:
parent
8ebaac1e2c
commit
4be339d4bf
5 changed files with 99 additions and 29 deletions
packages/console/src/pages
EnterpriseSso/SsoConnectorLogo
UserDetails/UserSettings/components
|
@ -6,6 +6,7 @@ import ImageWithErrorFallback from '@/ds-components/ImageWithErrorFallback';
|
|||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import { pickLogoForCurrentThemeHelper } from './utils';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
|
@ -23,9 +24,13 @@ const pickLogoForCurrentTheme = (
|
|||
branding: SsoConnectorWithProviderConfig['branding']
|
||||
): string => {
|
||||
// Need to use `||` here since `??` operator can not avoid empty strings.
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const configuredLogo = isDarkMode ? branding.darkLogo : branding.logo || branding.darkLogo;
|
||||
const builtInLogo = isDarkMode ? logoDark : logo || logoDark;
|
||||
// Since `logo` and `darkLogo` are both optional, when it is dark mode and `darkLogo` is not configured, should fallback to `logo`.
|
||||
const configuredLogo = pickLogoForCurrentThemeHelper(
|
||||
isDarkMode,
|
||||
branding.logo,
|
||||
branding.darkLogo
|
||||
);
|
||||
const builtInLogo = pickLogoForCurrentThemeHelper(isDarkMode, logo, logoDark);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
return configuredLogo || builtInLogo;
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { pickLogoForCurrentThemeHelper } from './utils';
|
||||
|
||||
describe('pickLogoForCurrentThemeHelper', () => {
|
||||
const logo = 'logo';
|
||||
const logoDark = 'logoDark';
|
||||
|
||||
it('dark mode, logo non-empty, logoDark non-empty, should get logoDark', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(true, logo, logoDark)).toBe(logoDark);
|
||||
});
|
||||
|
||||
it('light mode, logo non-empty, logoDark non-empty, should get logo', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(false, logo, logoDark)).toBe(logo);
|
||||
});
|
||||
|
||||
it('dark mode, logo non-empty, logoDark empty, should get logo', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(true, logo, '')).toBe(logo);
|
||||
});
|
||||
|
||||
it('light mode, logo non-empty, logoDark empty, should get logo', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(false, logo, '')).toBe(logo);
|
||||
});
|
||||
|
||||
it('dark mode, logo empty, logoDark non-empty, should get logoDark', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(true, '', logoDark)).toBe(logoDark);
|
||||
});
|
||||
|
||||
it('light mode, logo empty, logoDark non-empty, should get logoDark', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(false, '', logoDark)).toBe(logoDark);
|
||||
});
|
||||
|
||||
it('dark mode, logo empty, logoDark empty, should get empty string', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(true, '', '')).toBe('');
|
||||
});
|
||||
|
||||
it('light mode, logo empty, logoDark empty, should get empty string', () => {
|
||||
expect(pickLogoForCurrentThemeHelper(false, '', '')).toBe('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { type Optional } from '@silverhand/essentials';
|
||||
|
||||
export const pickLogoForCurrentThemeHelper = <T extends string | Optional<string>>(
|
||||
isDarkMode: boolean,
|
||||
logo: T,
|
||||
logoDark: T
|
||||
): T => {
|
||||
return (isDarkMode ? logoDark : logo) || logoDark || logo;
|
||||
};
|
|
@ -11,10 +11,14 @@
|
|||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: _.unit(2);
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import type { SsoConnectorWithProviderConfig, UserSsoIdentity } from '@logto/schemas';
|
||||
import {
|
||||
ssoBrandingGuard,
|
||||
type SsoConnectorWithProviderConfig,
|
||||
type UserSsoIdentity,
|
||||
} from '@logto/schemas';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
import { z } from 'zod';
|
||||
|
||||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import Table from '@/ds-components/Table';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import SsoConnectorLogo from '@/pages/EnterpriseSso/SsoConnectorLogo';
|
||||
|
||||
import * as styles from '../UserSocialIdentities/index.module.scss';
|
||||
|
@ -24,8 +28,21 @@ type DisplayConnector = {
|
|||
issuer: UserSsoIdentity['issuer'];
|
||||
};
|
||||
|
||||
const isIdentityDisplayConnector = (
|
||||
identity: Partial<DisplayConnector>
|
||||
): identity is DisplayConnector => {
|
||||
// `DisplayConnector` instance should have `providerLogo`, `providerLogoDark` and `name` to be non-empty string and `branding` to be an object.
|
||||
const identityGuard = z.object({
|
||||
providerLogo: z.string().min(1),
|
||||
providerLogoDark: z.string().min(1),
|
||||
branding: ssoBrandingGuard,
|
||||
name: z.string().min(1),
|
||||
});
|
||||
const result = identityGuard.safeParse(identity);
|
||||
return result.success;
|
||||
};
|
||||
|
||||
function UserSsoIdentities({ ssoIdentities }: Props) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation(undefined, {
|
||||
keyPrefix: 'admin_console',
|
||||
});
|
||||
|
@ -41,25 +58,19 @@ function UserSsoIdentities({ ssoIdentities }: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
ssoIdentities
|
||||
.map((identity) => {
|
||||
const {
|
||||
providerLogo,
|
||||
providerLogoDark,
|
||||
connectorName: name,
|
||||
} = data.find((ssoConnector) => ssoConnector.id === identity.ssoConnectorId) ?? {};
|
||||
const { identityId: userIdentity, issuer } = identity;
|
||||
return ssoIdentities
|
||||
.map((identity) => {
|
||||
const {
|
||||
providerLogo,
|
||||
providerLogoDark,
|
||||
branding,
|
||||
connectorName: name,
|
||||
} = data.find((ssoConnector) => ssoConnector.id === identity.ssoConnectorId) ?? {};
|
||||
const { identityId: userIdentity, issuer } = identity;
|
||||
|
||||
if (!(providerLogo && providerLogoDark && name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { providerLogo, providerLogoDark, name, userIdentity, issuer };
|
||||
})
|
||||
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
|
||||
.filter((identity): identity is DisplayConnector => Boolean(identity))
|
||||
);
|
||||
return { providerLogo, providerLogoDark, branding, name, userIdentity, issuer };
|
||||
})
|
||||
.filter((identity): identity is DisplayConnector => isIdentityDisplayConnector(identity));
|
||||
}, [data, ssoIdentities]);
|
||||
|
||||
const hasLinkedSsoIdentities = Boolean(displaySsoConnectors && displaySsoConnectors.length > 0);
|
||||
|
@ -89,7 +100,10 @@ function UserSsoIdentities({ ssoIdentities }: Props) {
|
|||
colSpan: 5,
|
||||
render: ({ providerLogo, providerLogoDark, branding, name }) => (
|
||||
<div className={styles.connectorName}>
|
||||
<SsoConnectorLogo data={{ providerLogo, providerLogoDark, branding }} />
|
||||
<SsoConnectorLogo
|
||||
containerClassName={styles.icon}
|
||||
data={{ providerLogo, providerLogoDark, branding }}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue