diff --git a/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/index.tsx b/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/index.tsx index bd19a0a16..dfc410173 100644 --- a/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/index.tsx +++ b/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/index.tsx @@ -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; diff --git a/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/utils.test.ts b/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/utils.test.ts new file mode 100644 index 000000000..06227cc8e --- /dev/null +++ b/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/utils.test.ts @@ -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(''); + }); +}); diff --git a/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/utils.ts b/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/utils.ts new file mode 100644 index 000000000..e7c3087dc --- /dev/null +++ b/packages/console/src/pages/EnterpriseSso/SsoConnectorLogo/utils.ts @@ -0,0 +1,9 @@ +import { type Optional } from '@silverhand/essentials'; + +export const pickLogoForCurrentThemeHelper = >( + isDarkMode: boolean, + logo: T, + logoDark: T +): T => { + return (isDarkMode ? logoDark : logo) || logoDark || logo; +}; diff --git a/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss b/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss index 36611ebc4..ea011097e 100644 --- a/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss +++ b/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss @@ -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 { diff --git a/packages/console/src/pages/UserDetails/UserSettings/components/UserSsoIdentities/index.tsx b/packages/console/src/pages/UserDetails/UserSettings/components/UserSsoIdentities/index.tsx index cbfc4e412..48fa201ef 100644 --- a/packages/console/src/pages/UserDetails/UserSettings/components/UserSsoIdentities/index.tsx +++ b/packages/console/src/pages/UserDetails/UserSettings/components/UserSsoIdentities/index.tsx @@ -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 +): 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 }) => (
- +
{name}