mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
Merge pull request #6226 from logto-io/gao-experience-branding-fixes
This commit is contained in:
commit
ef33361179
29 changed files with 86 additions and 60 deletions
|
@ -3,7 +3,7 @@ import { Controller, useFormContext } from 'react-hook-form';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import Error from '@/assets/icons/toast-error.svg';
|
||||
import LogoInputs from '@/components/ImageInputs';
|
||||
import ImageInputs from '@/components/ImageInputs';
|
||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import Select from '@/ds-components/Select';
|
||||
|
@ -57,7 +57,7 @@ function BasicForm({ isAllowEditTarget, isStandard, conflictConnectorName }: Pro
|
|||
{...register('name', { required: true })}
|
||||
/>
|
||||
</FormField>
|
||||
<LogoInputs
|
||||
<ImageInputs
|
||||
uploadTitle="connectors.guide.connector_logo"
|
||||
tip={t('connectors.guide.connector_logo_tip')}
|
||||
control={control}
|
||||
|
@ -65,7 +65,7 @@ function BasicForm({ isAllowEditTarget, isStandard, conflictConnectorName }: Pro
|
|||
fields={Object.values(Theme).map((theme) => ({
|
||||
name: themeToField[theme],
|
||||
error: errors[themeToField[theme]],
|
||||
type: 'app_logo',
|
||||
type: 'connector_logo',
|
||||
theme,
|
||||
}))}
|
||||
/>
|
||||
|
|
|
@ -45,7 +45,7 @@ function LogoAndFavicon<FormContext extends FieldValues>({
|
|||
uploadTitle={
|
||||
<>
|
||||
{t(`sign_in_exp.branding.with_${theme}`, {
|
||||
value: t('sign_in_exp.branding.app_logo_and_favicon'),
|
||||
value: t(`sign_in_exp.branding.${type}_and_favicon`),
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -2,24 +2,11 @@
|
|||
|
||||
.container {
|
||||
display: flex;
|
||||
gap: _.unit(2);
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: #111;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ import type { TipBubblePlacement } from '.';
|
|||
export const getVerticalOffset = (placement: TipBubblePlacement) => {
|
||||
switch (placement) {
|
||||
case 'top': {
|
||||
return -16;
|
||||
return -8;
|
||||
}
|
||||
|
||||
case 'bottom': {
|
||||
return 16;
|
||||
return 8;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
.actionDescription {
|
||||
margin-top: _.unit(1);
|
||||
font: var(--font-body-2); // With font height to be 20px.
|
||||
font: var(--font-body-3);
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
@ -60,4 +60,3 @@
|
|||
border-color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,3 +3,11 @@
|
|||
.colors {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.darkModeTip {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: _.unit(1);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
import { Theme, type Application, type ApplicationSignInExperience } from '@logto/schemas';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { generateDarkColor } from '@logto/core-kit';
|
||||
import {
|
||||
Theme,
|
||||
defaultPrimaryColor,
|
||||
type Application,
|
||||
type ApplicationSignInExperience,
|
||||
} from '@logto/schemas';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useForm, FormProvider, Controller } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DetailsForm from '@/components/DetailsForm';
|
||||
import FormCard, { FormCardSkeleton } from '@/components/FormCard';
|
||||
import LogoInputs, { themeToLogoName } from '@/components/ImageInputs';
|
||||
import ImageInputs, { themeToLogoName } from '@/components/ImageInputs';
|
||||
import LogoAndFavicon from '@/components/ImageInputs/LogoAndFavicon';
|
||||
import RequestDataError from '@/components/RequestDataError';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import { appSpecificBrandingLink, logtoThirdPartyAppBrandingLink } from '@/consts';
|
||||
import Button from '@/ds-components/Button';
|
||||
import ColorPicker from '@/ds-components/ColorPicker';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import Switch from '@/ds-components/Switch';
|
||||
|
@ -105,10 +112,22 @@ function Branding({ application, isActive }: Props) {
|
|||
// is valid; otherwise, directly save the form will be a no-op.
|
||||
useEffect(() => {
|
||||
if (isBrandingEnabled && Object.values(color).filter(Boolean).length === 0) {
|
||||
setValue('color', { primaryColor: '#000000', darkPrimaryColor: '#000000' });
|
||||
setValue('color', {
|
||||
primaryColor: defaultPrimaryColor,
|
||||
darkPrimaryColor: generateDarkColor(defaultPrimaryColor),
|
||||
});
|
||||
}
|
||||
}, [color, isBrandingEnabled, setValue]);
|
||||
|
||||
const [primaryColor, darkPrimaryColor] = watch(['color.primaryColor', 'color.darkPrimaryColor']);
|
||||
const calculatedDarkPrimaryColor = useMemo(() => {
|
||||
return primaryColor && generateDarkColor(primaryColor);
|
||||
}, [primaryColor]);
|
||||
|
||||
const handleResetColor = useCallback(() => {
|
||||
setValue('color.darkPrimaryColor', calculatedDarkPrimaryColor);
|
||||
}, [calculatedDarkPrimaryColor, setValue]);
|
||||
|
||||
const NonThirdPartyBrandingForm = useCallback(
|
||||
() => (
|
||||
<>
|
||||
|
@ -153,10 +172,29 @@ function Branding({ application, isActive }: Props) {
|
|||
</FormField>
|
||||
)}
|
||||
/>
|
||||
{calculatedDarkPrimaryColor !== darkPrimaryColor && (
|
||||
<div className={styles.darkModeTip}>
|
||||
{t('sign_in_exp.color.dark_mode_reset_tip')}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
title="sign_in_exp.color.reset"
|
||||
onClick={handleResetColor}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
[control, errors.branding, register]
|
||||
[
|
||||
control,
|
||||
errors.branding,
|
||||
register,
|
||||
calculatedDarkPrimaryColor,
|
||||
darkPrimaryColor,
|
||||
handleResetColor,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -193,7 +231,7 @@ function Branding({ application, isActive }: Props) {
|
|||
<FormField title="application_details.branding.display_name">
|
||||
<TextInput {...register('displayName')} placeholder={application.name} />
|
||||
</FormField>
|
||||
<LogoInputs
|
||||
<ImageInputs
|
||||
uploadTitle="application_details.branding.app_logo"
|
||||
control={control}
|
||||
register={register}
|
||||
|
|
|
@ -70,6 +70,12 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P
|
|||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const api = useApi();
|
||||
const hasBranding = [
|
||||
ApplicationType.Native,
|
||||
ApplicationType.SPA,
|
||||
ApplicationType.Traditional,
|
||||
ApplicationType.Protected,
|
||||
].includes(data.type);
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async (formData) => {
|
||||
|
@ -189,9 +195,7 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P
|
|||
{t('application_details.permissions.name')}
|
||||
</TabNavItem>
|
||||
)}
|
||||
{[ApplicationType.Native, ApplicationType.SPA, ApplicationType.Traditional].includes(
|
||||
data.type
|
||||
) && (
|
||||
{hasBranding && (
|
||||
<TabNavItem href={`/applications/${data.id}/${ApplicationDetailsTabs.Branding}`}>
|
||||
{t('application_details.branding.name')}
|
||||
</TabNavItem>
|
||||
|
@ -266,9 +270,7 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P
|
|||
<Permissions application={data} />
|
||||
</TabWrapper>
|
||||
)}
|
||||
{[ApplicationType.Native, ApplicationType.SPA, ApplicationType.Traditional].includes(
|
||||
data.type
|
||||
) && (
|
||||
{hasBranding && (
|
||||
<TabWrapper
|
||||
isActive={tab === ApplicationDetailsTabs.Branding}
|
||||
className={styles.tabContainer}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { RoleType, type SsoConnectorWithProviderConfig } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Controller, type UseFormReturn } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
@ -69,7 +70,6 @@ function JitSettings({ form }: Props) {
|
|||
description="organization_details.jit.description"
|
||||
>
|
||||
<FormField
|
||||
className={styles.jitFormField}
|
||||
title="organization_details.jit.enterprise_sso"
|
||||
description={
|
||||
<Trans
|
||||
|
@ -87,7 +87,7 @@ function JitSettings({ form }: Props) {
|
|||
descriptionPosition="top"
|
||||
>
|
||||
{!allSsoConnectors?.length && (
|
||||
<InlineNotification>
|
||||
<InlineNotification className={styles.ssoJitContent}>
|
||||
<Trans
|
||||
i18nKey="admin_console.organization_details.jit.no_enterprise_connector_set"
|
||||
components={{ a: <TextLink to={'/' + enterpriseSso.path} /> }}
|
||||
|
@ -100,7 +100,7 @@ function JitSettings({ form }: Props) {
|
|||
render={({ field: { onChange, value } }) => (
|
||||
<>
|
||||
{value.length > 0 && (
|
||||
<div className={styles.ssoConnectorList}>
|
||||
<div className={classNames(styles.ssoJitContent, styles.ssoConnectorList)}>
|
||||
{value.map((id) => {
|
||||
const connector = allSsoConnectors?.find(
|
||||
({ id: connectorId }) => id === connectorId
|
||||
|
@ -130,6 +130,7 @@ function JitSettings({ form }: Props) {
|
|||
{Boolean(filteredSsoConnectors?.length) && (
|
||||
<ActionMenu
|
||||
buttonProps={{
|
||||
className: styles.ssoJitContent,
|
||||
type: 'default',
|
||||
size: 'medium',
|
||||
title: 'organization_details.jit.add_enterprise_connector',
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.jitFormField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(3);
|
||||
.ssoJitContent {
|
||||
margin-top: _.unit(2);
|
||||
}
|
||||
|
||||
.mfaWarning {
|
||||
|
|
|
@ -7,7 +7,7 @@ import useSWR from 'swr';
|
|||
|
||||
import DetailsForm from '@/components/DetailsForm';
|
||||
import FormCard from '@/components/FormCard';
|
||||
import LogoInputs, { themeToLogoName } from '@/components/ImageInputs';
|
||||
import ImageInputs, { themeToLogoName } from '@/components/ImageInputs';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import { organizationLogosForExperienceLink } from '@/consts';
|
||||
import CodeEditor from '@/ds-components/CodeEditor';
|
||||
|
@ -107,7 +107,7 @@ function Settings() {
|
|||
{...register('description')}
|
||||
/>
|
||||
</FormField>
|
||||
<LogoInputs
|
||||
<ImageInputs
|
||||
uploadTitle="organization_details.branding.logo"
|
||||
tip={
|
||||
<Trans
|
||||
|
|
|
@ -85,7 +85,7 @@ function PlanComparisonTable() {
|
|||
// UI and branding
|
||||
const customDomain = t('branding.custom_domain');
|
||||
const customCss = t('branding.custom_css');
|
||||
const appLogoAndFavicon = t('branding.app_logo_and_favicon');
|
||||
const appLogoAndFavicon = t('branding.logo_and_favicon');
|
||||
const darkMode = t('branding.dark_mode');
|
||||
const i18n = t('branding.i18n');
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'Benutzeroberfläche und Branding',
|
||||
custom_domain: 'Benutzerdefinierte Domain',
|
||||
custom_css: 'Benutzerdefiniertes CSS',
|
||||
app_logo_and_favicon: 'App-Logo und Favicon',
|
||||
dark_mode: 'Dunkler Modus',
|
||||
i18n: 'Internationalisierung',
|
||||
},
|
||||
|
|
|
@ -36,6 +36,7 @@ const sign_in_exp = {
|
|||
with_light: '{{value}}',
|
||||
with_dark: '{{value}} (dark)',
|
||||
app_logo_and_favicon: 'App logo and favicon',
|
||||
company_logo_and_favicon: 'Company logo and favicon',
|
||||
},
|
||||
branding_uploads: {
|
||||
app_logo: {
|
||||
|
@ -56,6 +57,12 @@ const sign_in_exp = {
|
|||
url_placeholder: 'https://your.cdn.domain/logo.png',
|
||||
error: 'Organization logo: {{error}}',
|
||||
},
|
||||
connector_logo: {
|
||||
title: 'Upload image',
|
||||
url: 'Connector logo URL',
|
||||
url_placeholder: 'https://your.cdn.domain/logo.png',
|
||||
error: 'Connector logo: {{error}}',
|
||||
},
|
||||
favicon: {
|
||||
title: 'Favicon',
|
||||
url: 'Favicon URL',
|
||||
|
|
|
@ -20,7 +20,7 @@ const quota_table = {
|
|||
title: 'UI and branding',
|
||||
custom_domain: 'Custom domain',
|
||||
custom_css: 'Custom CSS',
|
||||
app_logo_and_favicon: 'App logo and favicon',
|
||||
logo_and_favicon: 'Logo and favicon',
|
||||
dark_mode: 'Dark mode',
|
||||
i18n: 'Internationalization',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'Interfaz de usuario y branding',
|
||||
custom_domain: 'Dominio personalizado',
|
||||
custom_css: 'CSS personalizado',
|
||||
app_logo_and_favicon: 'Logo de la aplicación y favicon',
|
||||
dark_mode: 'Modo oscuro',
|
||||
i18n: 'Internacionalización',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'Interface utilisateur et branding',
|
||||
custom_domain: 'Domaine personnalisé',
|
||||
custom_css: 'CSS personnalisé',
|
||||
app_logo_and_favicon: "Logo et favicon de l'application",
|
||||
dark_mode: 'Mode sombre',
|
||||
i18n: 'Internationalisation',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'UI e branding',
|
||||
custom_domain: 'Dominio personalizzato',
|
||||
custom_css: 'CSS personalizzato',
|
||||
app_logo_and_favicon: "Logo e favicon dell'applicazione",
|
||||
dark_mode: 'Modalità scura',
|
||||
i18n: 'Internazionalizzazione',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'UIとブランディング',
|
||||
custom_domain: 'カスタムドメイン',
|
||||
custom_css: 'カスタムCSS',
|
||||
app_logo_and_favicon: 'アプリロゴとFavicon',
|
||||
dark_mode: 'ダークモード',
|
||||
i18n: '国際化',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'UI 및 브랜딩',
|
||||
custom_domain: '사용자 정의 도메인',
|
||||
custom_css: '사용자 정의 CSS',
|
||||
app_logo_and_favicon: '앱 로고와 파비콘',
|
||||
dark_mode: '다크 모드',
|
||||
i18n: '국제화',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'Interfejs użytkownika i branding',
|
||||
custom_domain: 'Domena niestandardowa',
|
||||
custom_css: 'Niestandardowy CSS',
|
||||
app_logo_and_favicon: 'Logo aplikacji i ikona',
|
||||
dark_mode: 'Tryb ciemny',
|
||||
i18n: 'Internacjonalizacja',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'Interface de usuário e branding',
|
||||
custom_domain: 'Domínio personalizado',
|
||||
custom_css: 'CSS personalizado',
|
||||
app_logo_and_favicon: 'Logotipo da aplicação e favicon',
|
||||
dark_mode: 'Modo escuro',
|
||||
i18n: 'Internacionalização',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'UI e branding',
|
||||
custom_domain: 'Domínio personalizado',
|
||||
custom_css: 'CSS personalizado',
|
||||
app_logo_and_favicon: 'Logótipo da aplicação e favicon',
|
||||
dark_mode: 'Modo escuro',
|
||||
i18n: 'Internacionalização',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'Интерфейс и брендинг',
|
||||
custom_domain: 'Пользовательский домен',
|
||||
custom_css: 'Пользовательский CSS',
|
||||
app_logo_and_favicon: 'Логотип и фавикон приложения',
|
||||
dark_mode: 'Темный режим',
|
||||
i18n: 'Интернационализация',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: 'Kullanıcı Arayüzü ve Markalama',
|
||||
custom_domain: 'Özel alan adı',
|
||||
custom_css: 'Özel CSS',
|
||||
app_logo_and_favicon: 'Uygulama logoları ve favicon',
|
||||
dark_mode: 'Karanlık mod',
|
||||
i18n: 'Uluslararasılaştırma',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: '界面与品牌',
|
||||
custom_domain: '自定义域名',
|
||||
custom_css: '自定义 CSS',
|
||||
app_logo_and_favicon: '应用图标与网站图标',
|
||||
dark_mode: '深色模式',
|
||||
i18n: '国际化',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: '用戶界面與品牌',
|
||||
custom_domain: '自訂網域',
|
||||
custom_css: '自訂 CSS',
|
||||
app_logo_and_favicon: '應用程式徽標和網站圖示',
|
||||
dark_mode: '深色模式',
|
||||
i18n: '國際化',
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ const quota_table = {
|
|||
title: '使用者介面和品牌塑造',
|
||||
custom_domain: '自訂網域',
|
||||
custom_css: '自訂 CSS',
|
||||
app_logo_and_favicon: '應用程式標誌和網站圖示',
|
||||
dark_mode: '深色模式',
|
||||
i18n: '國際化',
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MfaPolicy, SignInIdentifier } from '../foundations/index.js';
|
|||
|
||||
import { adminTenantId, defaultTenantId } from './tenant.js';
|
||||
|
||||
const defaultPrimaryColor = '#6139F6';
|
||||
export const defaultPrimaryColor = '#6139F6';
|
||||
|
||||
export const createDefaultSignInExperience = (
|
||||
forTenantId: string,
|
||||
|
|
Loading…
Add table
Reference in a new issue