0
Fork 0
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:
Gao Sun 2024-07-12 15:56:05 +08:00 committed by GitHub
commit ef33361179
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 86 additions and 60 deletions

View file

@ -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,
}))}
/>

View file

@ -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`),
})}
</>
}

View file

@ -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;
}

View file

@ -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: {

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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}

View file

@ -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}

View file

@ -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',

View file

@ -16,10 +16,8 @@
}
}
.jitFormField {
display: flex;
flex-direction: column;
gap: _.unit(3);
.ssoJitContent {
margin-top: _.unit(2);
}
.mfaWarning {

View file

@ -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

View file

@ -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');

View file

@ -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',
},

View file

@ -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',

View file

@ -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',
},

View file

@ -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',
},

View file

@ -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',
},

View file

@ -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',
},

View file

@ -20,7 +20,6 @@ const quota_table = {
title: 'UIとブランディング',
custom_domain: 'カスタムドメイン',
custom_css: 'カスタムCSS',
app_logo_and_favicon: 'アプリロゴとFavicon',
dark_mode: 'ダークモード',
i18n: '国際化',
},

View file

@ -20,7 +20,6 @@ const quota_table = {
title: 'UI 및 브랜딩',
custom_domain: '사용자 정의 도메인',
custom_css: '사용자 정의 CSS',
app_logo_and_favicon: '앱 로고와 파비콘',
dark_mode: '다크 모드',
i18n: '국제화',
},

View file

@ -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',
},

View file

@ -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',
},

View file

@ -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',
},

View file

@ -20,7 +20,6 @@ const quota_table = {
title: 'Интерфейс и брендинг',
custom_domain: 'Пользовательский домен',
custom_css: 'Пользовательский CSS',
app_logo_and_favicon: 'Логотип и фавикон приложения',
dark_mode: 'Темный режим',
i18n: 'Интернационализация',
},

View file

@ -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',
},

View file

@ -20,7 +20,6 @@ const quota_table = {
title: '界面与品牌',
custom_domain: '自定义域名',
custom_css: '自定义 CSS',
app_logo_and_favicon: '应用图标与网站图标',
dark_mode: '深色模式',
i18n: '国际化',
},

View file

@ -20,7 +20,6 @@ const quota_table = {
title: '用戶界面與品牌',
custom_domain: '自訂網域',
custom_css: '自訂 CSS',
app_logo_and_favicon: '應用程式徽標和網站圖示',
dark_mode: '深色模式',
i18n: '國際化',
},

View file

@ -20,7 +20,6 @@ const quota_table = {
title: '使用者介面和品牌塑造',
custom_domain: '自訂網域',
custom_css: '自訂 CSS',
app_logo_and_favicon: '應用程式標誌和網站圖示',
dark_mode: '深色模式',
i18n: '國際化',
},

View file

@ -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,