From 2c413341d1c515049faa130416f7a5e591d10e8a Mon Sep 17 00:00:00 2001 From: Wang Sijie Date: Fri, 24 Jun 2022 10:26:30 +0800 Subject: [PATCH] feat(console): sie form reorg (#1218) --- packages/console/src/App.tsx | 2 +- .../components/BrandingForm.tsx | 30 +---------- .../SignInExperience/components/ColorForm.tsx | 51 +++++++++++++++++++ .../components/GuideModal.tsx | 4 ++ .../src/pages/SignInExperience/index.tsx | 16 ++++-- .../src/pages/SignInExperience/utilities.ts | 12 +++-- .../core/src/__mocks__/sign-in-experience.ts | 10 +++- .../src/queries/sign-in-experience.test.ts | 3 +- .../sign-in-experience.branding.guard.test.ts | 24 --------- .../sign-in-experience.color.guard.test.ts | 44 ++++++++++++++++ .../src/routes/sign-in-experience.test.ts | 3 ++ packages/phrases/src/locales/en.ts | 11 ++-- packages/phrases/src/locales/zh-cn.ts | 17 ++++--- .../schemas/src/foundations/jsonb-types.ts | 11 ++-- .../schemas/src/seeds/sign-in-experience.ts | 4 +- .../schemas/tables/sign_in_experiences.sql | 1 + packages/ui/src/__mocks__/logto.tsx | 5 +- .../ui/src/containers/AppContent/index.tsx | 5 +- packages/ui/src/hooks/use-preview.ts | 6 +-- packages/ui/src/hooks/use-theme.ts | 2 +- packages/ui/src/types/index.ts | 2 + 21 files changed, 174 insertions(+), 89 deletions(-) create mode 100644 packages/console/src/pages/SignInExperience/components/ColorForm.tsx create mode 100644 packages/core/src/routes/sign-in-experience.color.guard.test.ts diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index 23eb843ae..15840b70e 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -73,7 +73,7 @@ const Main = () => { } /> - } /> + } /> } /> } /> diff --git a/packages/console/src/pages/SignInExperience/components/BrandingForm.tsx b/packages/console/src/pages/SignInExperience/components/BrandingForm.tsx index 197bf1328..de1bab29a 100644 --- a/packages/console/src/pages/SignInExperience/components/BrandingForm.tsx +++ b/packages/console/src/pages/SignInExperience/components/BrandingForm.tsx @@ -3,10 +3,8 @@ import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import ColorPicker from '@/components/ColorPicker'; import FormField from '@/components/FormField'; import RadioGroup, { Radio } from '@/components/RadioGroup'; -import Switch from '@/components/Switch'; import TextInput from '@/components/TextInput'; import { uriValidator } from '@/utilities/validator'; @@ -22,39 +20,13 @@ const BrandingForm = () => { formState: { errors }, } = useFormContext(); - const isDarkModeEnabled = watch('branding.isDarkModeEnabled'); + const isDarkModeEnabled = watch('color.isDarkModeEnabled'); const style = watch('branding.style'); const isSloganRequired = style === BrandingStyle.Logo_Slogan; return ( <>
{t('sign_in_exp.branding.title')}
- - ( - - )} - /> - - - - - {isDarkModeEnabled && ( - - ( - - )} - /> - - )} { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { watch, register, control } = useFormContext(); + + const isDarkModeEnabled = watch('color.isDarkModeEnabled'); + + return ( + <> +
{t('sign_in_exp.color.title')}
+ + ( + + )} + /> + + + + + {isDarkModeEnabled && ( + + ( + + )} + /> + + )} + + ); +}; + +export default ColorForm; diff --git a/packages/console/src/pages/SignInExperience/components/GuideModal.tsx b/packages/console/src/pages/SignInExperience/components/GuideModal.tsx index 6df1e971c..0df4ce582 100644 --- a/packages/console/src/pages/SignInExperience/components/GuideModal.tsx +++ b/packages/console/src/pages/SignInExperience/components/GuideModal.tsx @@ -20,6 +20,7 @@ import usePreviewConfigs from '../hooks'; import { SignInExperienceForm } from '../types'; import { signInExperienceParser } from '../utilities'; import BrandingForm from './BrandingForm'; +import ColorForm from './ColorForm'; import * as styles from './GuideModal.module.scss'; import LanguagesForm from './LanguagesForm'; import Preview from './Preview'; @@ -100,6 +101,9 @@ const GuideModal = ({ isOpen, onClose }: Props) => {
+
+ +
diff --git a/packages/console/src/pages/SignInExperience/index.tsx b/packages/console/src/pages/SignInExperience/index.tsx index 8e4940a35..375e58410 100644 --- a/packages/console/src/pages/SignInExperience/index.tsx +++ b/packages/console/src/pages/SignInExperience/index.tsx @@ -17,6 +17,7 @@ import useSettings from '@/hooks/use-settings'; import * as detailsStyles from '@/scss/details.module.scss'; import BrandingForm from './components/BrandingForm'; +import ColorForm from './components/ColorForm'; import LanguagesForm from './components/LanguagesForm'; import Preview from './components/Preview'; import SignInMethodsChangePreview from './components/SignInMethodsChangePreview'; @@ -100,8 +101,8 @@ const SignInExperience = () => { - - {t('sign_in_exp.tabs.experience')} + + {t('sign_in_exp.tabs.branding')} {t('sign_in_exp.tabs.methods')} @@ -116,14 +117,19 @@ const SignInExperience = () => {
- {tab === 'experience' && ( + {tab === 'branding' && ( <> + - )} {tab === 'methods' && } - {tab === 'others' && } + {tab === 'others' && ( + <> + + + + )}
diff --git a/packages/console/src/pages/SignInExperience/utilities.ts b/packages/console/src/pages/SignInExperience/utilities.ts index a2f28903e..06931c531 100644 --- a/packages/console/src/pages/SignInExperience/utilities.ts +++ b/packages/console/src/pages/SignInExperience/utilities.ts @@ -4,6 +4,7 @@ import { SignInMethods, SignInMethodState, } from '@logto/schemas'; +import { conditional } from '@silverhand/essentials'; import { LanguageMode, SignInExperienceForm } from './types'; @@ -57,18 +58,23 @@ export const signInExperienceParser = { }, toRemoteModel: (setup: SignInExperienceForm): SignInExperience => { const { + color, branding, languageInfo: { mode, fallbackLanguage, fixedLanguage }, } = setup; return { ...setup, + color: { + ...color, + // Transform empty string to undefined + darkPrimaryColor: conditional(color.darkPrimaryColor?.length && color.darkPrimaryColor), + }, branding: { ...branding, // Transform empty string to undefined - darkPrimaryColor: branding.darkPrimaryColor?.length ? branding.darkPrimaryColor : undefined, - darkLogoUrl: branding.darkLogoUrl?.length ? branding.darkLogoUrl : undefined, - slogan: branding.slogan?.length ? branding.slogan : undefined, + darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl), + slogan: conditional(branding.slogan?.length && branding.slogan), }, signInMethods: { username: findMethodState(setup, 'username'), diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index a49d1cd89..95c6f0bd0 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -8,14 +8,17 @@ import { SignInMethodState, TermsOfUse, SignInMode, + Color, } from '@logto/schemas'; export const mockSignInExperience: SignInExperience = { id: 'foo', - branding: { + color: { primaryColor: '#000', isDarkModeEnabled: true, darkPrimaryColor: '#fff', + }, + branding: { style: BrandingStyle.Logo, logoUrl: 'http://logto.png', slogan: 'logto', @@ -38,10 +41,13 @@ export const mockSignInExperience: SignInExperience = { signInMode: SignInMode.SignInAndRegister, }; -export const mockBranding: Branding = { +export const mockColor: Color = { primaryColor: '#000', isDarkModeEnabled: true, darkPrimaryColor: '#fff', +}; + +export const mockBranding: Branding = { style: BrandingStyle.Logo_Slogan, logoUrl: 'http://silverhand.png', slogan: 'Silverhand.', diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index 09f8f123b..2be2a7ab9 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -21,6 +21,7 @@ describe('sign-in-experience query', () => { const dbvalue = { ...mockSignInExperience, + color: JSON.stringify(mockSignInExperience.color), branding: JSON.stringify(mockSignInExperience.branding), termsOfUse: JSON.stringify(mockSignInExperience.termsOfUse), languageInfo: JSON.stringify(mockSignInExperience.languageInfo), @@ -31,7 +32,7 @@ describe('sign-in-experience query', () => { it('findDefaultSignInExperience', async () => { /* eslint-disable sql/no-unsafe-query */ const expectSql = ` - select "id", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_targets", "sign_in_mode" + select "id", "color", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_targets", "sign_in_mode" from "sign_in_experiences" where "id" = $1 `; diff --git a/packages/core/src/routes/sign-in-experience.branding.guard.test.ts b/packages/core/src/routes/sign-in-experience.branding.guard.test.ts index 5a72210c7..4603b0773 100644 --- a/packages/core/src/routes/sign-in-experience.branding.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience.branding.guard.test.ts @@ -26,30 +26,6 @@ const expectPatchResponseStatus = async (signInExperience: any, status: number) }; describe('branding', () => { - const colorKeys = ['primaryColor', 'darkPrimaryColor']; - const invalidColors = [null, '#0']; - - describe('colors', () => { - test.each(invalidColors)('should fail when color is %p', async (invalidColor) => { - for (const colorKey of colorKeys) { - // eslint-disable-next-line no-await-in-loop - await expectPatchResponseStatus( - { branding: { ...mockBranding, [colorKey]: invalidColor } }, - 400 - ); - } - }); - it('should succeed when color is valid', async () => { - for (const colorKey of colorKeys) { - // eslint-disable-next-line no-await-in-loop - await expectPatchResponseStatus( - { branding: { ...mockBranding, [colorKey]: '#169deF' } }, - 200 - ); - } - }); - }); - describe('style', () => { test.each(Object.values(BrandingStyle))('%p should succeed', async (style) => { const signInExperience = { branding: { ...mockBranding, style } }; diff --git a/packages/core/src/routes/sign-in-experience.color.guard.test.ts b/packages/core/src/routes/sign-in-experience.color.guard.test.ts new file mode 100644 index 000000000..dffdecddb --- /dev/null +++ b/packages/core/src/routes/sign-in-experience.color.guard.test.ts @@ -0,0 +1,44 @@ +import { CreateSignInExperience, SignInExperience } from '@logto/schemas'; + +import { mockColor, mockSignInExperience } from '@/__mocks__'; +import { createRequester } from '@/utils/test-utils'; + +import signInExperiencesRoutes from './sign-in-experience'; + +jest.mock('@/queries/sign-in-experience', () => ({ + updateDefaultSignInExperience: jest.fn( + async (data: Partial): Promise => ({ + ...mockSignInExperience, + ...data, + }) + ), +})); + +jest.mock('@/connectors', () => ({ + getConnectorInstances: jest.fn(async () => []), +})); + +const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); + +const expectPatchResponseStatus = async (signInExperience: any, status: number) => { + const response = await signInExperienceRequester.patch('/sign-in-exp').send(signInExperience); + expect(response.status).toEqual(status); +}; + +const colorKeys = ['primaryColor', 'darkPrimaryColor']; +const invalidColors = [null, '#0']; + +describe('colors', () => { + test.each(invalidColors)('should fail when color is %p', async (invalidColor) => { + for (const colorKey of colorKeys) { + // eslint-disable-next-line no-await-in-loop + await expectPatchResponseStatus({ color: { ...mockColor, [colorKey]: invalidColor } }, 400); + } + }); + it('should succeed when color is valid', async () => { + for (const colorKey of colorKeys) { + // eslint-disable-next-line no-await-in-loop + await expectPatchResponseStatus({ color: { ...mockColor, [colorKey]: '#169deF' } }, 200); + } + }); +}); diff --git a/packages/core/src/routes/sign-in-experience.test.ts b/packages/core/src/routes/sign-in-experience.test.ts index 75d3cf702..8dc9ce200 100644 --- a/packages/core/src/routes/sign-in-experience.test.ts +++ b/packages/core/src/routes/sign-in-experience.test.ts @@ -13,6 +13,7 @@ import { mockSignInExperience, mockSignInMethods, mockWechatConnectorInstance, + mockColor, } from '@/__mocks__'; import * as signInExpLib from '@/lib/sign-in-experience'; import { createRequester } from '@/utils/test-utils'; @@ -142,6 +143,7 @@ describe('PATCH /sign-in-exp', () => { const validateSignInMethods = jest.spyOn(signInExpLib, 'validateSignInMethods'); const response = await signInExperienceRequester.patch('/sign-in-exp').send({ + color: mockColor, branding: mockBranding, termsOfUse, signInMethods: mockSignInMethods, @@ -160,6 +162,7 @@ describe('PATCH /sign-in-exp', () => { status: 200, body: { ...mockSignInExperience, + color: mockColor, branding: mockBranding, termsOfUse, signInMethods: mockSignInMethods, diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 953fe65d1..24dc54a11 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -406,7 +406,7 @@ const translation = { title: 'Sign-in Experience', description: 'Customize the sign in UI to match your brand and preview in real time.', tabs: { - experience: 'Experience', + branding: 'Branding', methods: 'Sign in methods', others: 'Others', }, @@ -418,14 +418,17 @@ const translation = { 'Please note that sign-in experience will apply to all applications under this account.', got_it: 'Got it', }, - branding: { - title: 'BRANDING', + color: { + title: 'COLOR', primary_color: 'Brand color', dark_primary_color: 'Brand color (Dark)', dark_mode: 'Enable dark mode', dark_mode_description: 'Your app will have an auto-generated dark mode theme based on your brand color and Logto algorithm. You are free to customize.', - ui_style: 'Branding area', + }, + branding: { + title: 'BRANDING AREA', + ui_style: 'Style', styles: { logo_slogan: 'App logo with slogan', logo: 'App logo', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index f46bcde35..ae88e06c3 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -396,7 +396,7 @@ const translation = { title: '登录体验', description: '自定义登录界面,并实时预览真实效果。', tabs: { - experience: '体验', + branding: '品牌', methods: '登录方式', others: '其它', }, @@ -406,14 +406,17 @@ const translation = { apply_remind: '请注意,登录体验将会应用到当前账户下的所有应用。', got_it: '知道了', }, - branding: { - title: '品牌', + color: { + title: '颜色', primary_color: '品牌颜色', - dark_primary_color: '品牌颜色 (暗黑)', - dark_mode: '开启暗黑模式', + dark_primary_color: '品牌颜色 (深色)', + dark_mode: '开启深色模式', dark_mode_description: - '基于你的品牌颜色和 Logto 的算法,你的应用将会有一个自动生成的暗黑模式。当然,你可以自定义和修改。', - ui_style: '品牌定制区', + '基于你的品牌颜色和 Logto 的算法,你的应用将会有一个自动生成的深色模式。当然,你可以自定义和修改。', + }, + branding: { + title: '品牌定制区', + ui_style: '样式', styles: { logo_slogan: 'App logo 和标语', logo: '仅有Logo', diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 544d097ab..3d1e747bc 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -72,15 +72,20 @@ export type Identities = z.infer; * SignIn Experiences */ +export const colorGuard = z.object({ + primaryColor: z.string().regex(hexColorRegEx), + isDarkModeEnabled: z.boolean(), + darkPrimaryColor: z.string().regex(hexColorRegEx).optional(), +}); + +export type Color = z.infer; + export enum BrandingStyle { Logo = 'Logo', Logo_Slogan = 'Logo_Slogan', } export const brandingGuard = z.object({ - primaryColor: z.string().regex(hexColorRegEx), - isDarkModeEnabled: z.boolean(), - darkPrimaryColor: z.string().regex(hexColorRegEx).optional(), style: z.nativeEnum(BrandingStyle), logoUrl: z.string().url(), darkLogoUrl: z.string().url().optional(), diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index a773dba24..bfdcfcee1 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -5,10 +5,12 @@ import { BrandingStyle, SignInMethodState } from '../foundations'; export const defaultSignInExperience: Readonly = { id: 'default', - branding: { + color: { primaryColor: '#6139F6', isDarkModeEnabled: false, darkPrimaryColor: '#6139F6', + }, + branding: { style: BrandingStyle.Logo, logoUrl: 'https://logto.io/logo.svg', darkLogoUrl: 'https://logto.io/logo.svg', diff --git a/packages/schemas/tables/sign_in_experiences.sql b/packages/schemas/tables/sign_in_experiences.sql index 02e8ebf78..dd8e2b3bf 100644 --- a/packages/schemas/tables/sign_in_experiences.sql +++ b/packages/schemas/tables/sign_in_experiences.sql @@ -2,6 +2,7 @@ create type sign_in_mode as enum ('SignIn', 'Register', 'SignInAndRegister'); create table sign_in_experiences ( id varchar(21) not null, + color jsonb /* @use Color */ not null, branding jsonb /* @use Branding */ not null, language_info jsonb /* @use LanguageInfo */ not null, terms_of_use jsonb /* @use TermsOfUse */ not null, diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index 9777d2750..04792c2f7 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -123,10 +123,12 @@ export const mockSocialConnectorData = { export const mockSignInExperience: SignInExperience = { id: 'foo', - branding: { + color: { primaryColor: '#000', isDarkModeEnabled: true, darkPrimaryColor: '#fff', + }, + branding: { style: BrandingStyle.Logo_Slogan, logoUrl: 'http://logto.png', slogan: 'logto', @@ -151,6 +153,7 @@ export const mockSignInExperience: SignInExperience = { }; export const mockSignInExperienceSettings: SignInExperienceSettings = { + color: mockSignInExperience.color, branding: mockSignInExperience.branding, termsOfUse: mockSignInExperience.termsOfUse, languageInfo: mockSignInExperience.languageInfo, diff --git a/packages/ui/src/containers/AppContent/index.tsx b/packages/ui/src/containers/AppContent/index.tsx index e5784f99b..8da3335f2 100644 --- a/packages/ui/src/containers/AppContent/index.tsx +++ b/packages/ui/src/containers/AppContent/index.tsx @@ -22,10 +22,7 @@ const AppContent = ({ children }: Props) => { }, [setToast]); // Set Primary Color - useColorTheme( - experienceSettings?.branding.primaryColor, - experienceSettings?.branding.darkPrimaryColor - ); + useColorTheme(experienceSettings?.color.primaryColor, experienceSettings?.color.darkPrimaryColor); // Set Theme Mode useEffect(() => { diff --git a/packages/ui/src/hooks/use-preview.ts b/packages/ui/src/hooks/use-preview.ts index 03b634741..c2607da84 100644 --- a/packages/ui/src/hooks/use-preview.ts +++ b/packages/ui/src/hooks/use-preview.ts @@ -60,7 +60,7 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => { } const { - signInExperience: { signInMethods, socialConnectors, branding, ...rest }, + signInExperience: { signInMethods, socialConnectors, color, ...rest }, language, mode, platform, @@ -68,8 +68,8 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => { const experienceSettings: SignInExperienceSettings = { ...rest, - branding: { - ...branding, + color: { + ...color, isDarkModeEnabled: false, // Disable theme mode auto detection on preview }, primarySignInMethod: getPrimarySignInMethod(signInMethods), diff --git a/packages/ui/src/hooks/use-theme.ts b/packages/ui/src/hooks/use-theme.ts index d4a4056a3..289428390 100644 --- a/packages/ui/src/hooks/use-theme.ts +++ b/packages/ui/src/hooks/use-theme.ts @@ -11,7 +11,7 @@ export default function useTheme(): Theme { const { experienceSettings, theme, setTheme } = useContext(PageContext); useEffect(() => { - if (!experienceSettings?.branding.isDarkModeEnabled) { + if (!experienceSettings?.color.isDarkModeEnabled) { return; } diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts index 57853986f..11398edb8 100644 --- a/packages/ui/src/types/index.ts +++ b/packages/ui/src/types/index.ts @@ -5,6 +5,7 @@ import { SignInExperience, ConnectorMetadata, SignInMode, + Color, } from '@logto/schemas'; export type UserFlow = 'sign-in' | 'register'; @@ -30,6 +31,7 @@ export type SignInExperienceSettingsResponse = SignInExperience & { }; export type SignInExperienceSettings = { + color: Color; branding: Branding; languageInfo: LanguageInfo; termsOfUse: TermsOfUse;