0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): enable AC config and real-time live preview on customCss (#3325)

This commit is contained in:
Darcy Ye 2023-03-13 11:41:57 +08:00 committed by GitHub
parent 5784cfeb18
commit 268679b02e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 137 additions and 7 deletions

View file

@ -0,0 +1,8 @@
---
"@logto/console": minor
"@logto/phrases": minor
"@logto/ui": minor
---
Add custom CSS code editor so that users can apply advanced UI customization.
- Users can check the real time preview of the CSS via SIE preview on the right side.

View file

@ -29,6 +29,7 @@ const SignInExperiencePreview = ({ platform, mode, language = 'en', signInExperi
const { customPhrases } = useUiLanguages();
const { userEndpoint } = useContext(AppEndpointsContext);
const previewRef = useRef<HTMLIFrameElement>(null);
const customCssRef = useRef(document.createElement('style'));
const { data: allConnectors } = useSWR<ConnectorResponse[], RequestError>('api/connectors');
const configForUiPage = useMemo(() => {
@ -80,6 +81,8 @@ const SignInExperiencePreview = ({ platform, mode, language = 'en', signInExperi
useEffect(() => {
postPreviewMessage();
document.head.append(customCssRef.current);
const iframe = previewRef.current;
iframe?.addEventListener('load', postPreviewMessage);

View file

@ -0,0 +1,60 @@
import { useFormContext, Controller } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import Card from '@/components/Card';
import CodeEditor from '@/components/CodeEditor';
import FormField from '@/components/FormField';
import TextLink from '@/components/TextLink';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import type { SignInExperienceForm } from '../../types';
import * as tabsStyles from '../index.module.scss';
import * as brandingStyles from './index.module.scss';
const CustomCssForm = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
const { control } = useFormContext<SignInExperienceForm>();
return (
<Card>
<div className={tabsStyles.title}>{t('sign_in_exp.custom_css.title')}</div>
<FormField
title="sign_in_exp.custom_css.css_code_editor_title"
tip={(closeTipHandler) => (
<Trans
components={{
a: (
<TextLink
// TODO: update the link when Custom CSS docs are ready
to={getDocumentationUrl('/docs/recipes/customize-sie/configure-branding')}
target="_blank"
onClick={closeTipHandler}
/>
),
}}
>
{t('sign_in_exp.custom_css.css_code_editor_description', {
link: t('sign_in_exp.custom_css.css_code_editor_description_link_content'),
})}
</Trans>
)}
>
<Controller
name="customCss"
control={control}
render={({ field: { onChange, value } }) => (
<CodeEditor
className={brandingStyles.customCssCodeEditor}
language="scss"
value={value ?? undefined}
onChange={onChange}
/>
)}
/>
</FormField>
</Card>
);
};
export default CustomCssForm;

View file

@ -0,0 +1,5 @@
.customCssCodeEditor {
max-height: calc(100vh - 260px);
min-height: 111px; // min-height to show three lines of code
overflow-y: auto;
}

View file

@ -2,6 +2,7 @@ import TabWrapper from '../../components/TabWrapper';
import * as styles from '../index.module.scss';
import BrandingForm from './BrandingForm';
import ColorForm from './ColorForm';
import CustomCssForm from './CustomCssForm';
type Props = {
isActive: boolean;
@ -11,6 +12,7 @@ const Branding = ({ isActive }: Props) => (
<TabWrapper isActive={isActive} className={styles.tabContent}>
<ColorForm />
<BrandingForm />
<CustomCssForm />
</TabWrapper>
);

View file

@ -40,7 +40,7 @@ export const signInExperienceParser = {
};
},
toRemoteModel: (setup: SignInExperienceForm): SignInExperience => {
const { branding, createAccountEnabled, signUp } = setup;
const { branding, createAccountEnabled, signUp, customCss } = setup;
return {
...setup,
@ -59,6 +59,7 @@ export const signInExperienceParser = {
verify: false,
},
signInMode: createAccountEnabled ? SignInMode.SignInAndRegister : SignInMode.SignIn,
customCss: customCss?.length ? customCss : null,
};
},
};
@ -80,11 +81,12 @@ export const hasSignUpAndSignInConfigChanged = (
export const getBrandingErrorCount = (
errors: FieldErrorsImpl<DeepRequired<SignInExperienceForm>>
) => {
const { color, branding } = errors;
const { color, branding, customCss } = errors;
const colorFormErrorCount = color ? Object.keys(color).length : 0;
const brandingFormErrorCount = branding ? Object.keys(branding).length : 0;
const customCssFormErrorCount = customCss ? 1 : 0;
return colorFormErrorCount + brandingFormErrorCount;
return colorFormErrorCount + brandingFormErrorCount + customCssFormErrorCount;
};
export const getSignUpAndSignInErrorCount = (

View file

@ -90,6 +90,12 @@ const sign_in_exp = {
dark_logo_image_url: 'App logo URL (Dunkler Modus)',
dark_logo_image_url_placeholder: 'https://dein.cdn.domain/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
others: {
terms_of_use: {
title: 'NUTZUNGSBEDINGUNGEN',

View file

@ -33,6 +33,12 @@ const sign_in_exp = {
dark_logo_image_url: 'App logo image URL (Dark)',
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
sign_up_and_sign_in: {
identifiers_email: 'Email address',
identifiers_phone: 'Phone number',

View file

@ -35,6 +35,12 @@ const sign_in_exp = {
dark_logo_image_url: "URL de l'image du logo de l'application (Sombre)",
dark_logo_image_url_placeholder: 'https://votre.domaine.cdn/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
sign_up_and_sign_in: {
identifiers_email: 'Email address', // UNTRANSLATED
identifiers_phone: 'Phone number', // UNTRANSLATED

View file

@ -31,6 +31,12 @@ const sign_in_exp = {
dark_logo_image_url: '앱 로고 이미지 URL (다크 모드)',
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
sign_up_and_sign_in: {
identifiers_email: '이메일 주소',
identifiers_phone: '휴대전화번호',

View file

@ -34,6 +34,12 @@ const sign_in_exp = {
dark_logo_image_url: 'URL da imagem do logotipo do aplicativo (Escuro)',
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
sign_up_and_sign_in: {
identifiers_email: 'Endereço de e-mail',
identifiers_phone: 'Número de telefone',

View file

@ -33,6 +33,12 @@ const sign_in_exp = {
dark_logo_image_url: 'URL do logotipo da app (tema escuro)',
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
sign_up_and_sign_in: {
identifiers_email: 'Email address', // UNTRANSLATED
identifiers_phone: 'Phone number', // UNTRANSLATED

View file

@ -34,6 +34,12 @@ const sign_in_exp = {
dark_logo_image_url: 'Uygulama logosu resim URLi (Koyu)',
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
sign_up_and_sign_in: {
identifiers_email: 'Email address', // UNTRANSLATED
identifiers_phone: 'Phone number', // UNTRANSLATED

View file

@ -32,6 +32,12 @@ const sign_in_exp = {
dark_logo_image_url: 'Logo 图片 URL (深色)',
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
},
custom_css: {
title: 'CUSTOM CSS', // UNTRANSLATED
css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED
css_code_editor_description: 'Description - Doc. <a>{{link}}</a>', // UNTRANSLATED
css_code_editor_description_link_content: 'Readme', // UNTRANSLATED
},
sign_up_and_sign_in: {
identifiers_email: '邮件地址',
identifiers_phone: '手机号码',

View file

@ -29,10 +29,9 @@ import './scss/normalized.scss';
const App = () => {
const { context, Provider } = usePageContext();
const { isPreview, experienceSettings, setLoading, setExperienceSettings } = context;
const { experienceSettings, setLoading, setExperienceSettings } = context;
const customCssRef = useRef(document.createElement('style'));
usePreview(context);
const [isPreview, previewConfig] = usePreview(context);
useEffect(() => {
document.head.append(customCssRef.current);
@ -40,6 +39,9 @@ const App = () => {
useEffect(() => {
if (isPreview) {
// eslint-disable-next-line @silverhand/fp/no-mutation
customCssRef.current.textContent = previewConfig?.signInExperience.customCss ?? null;
return;
}
@ -61,7 +63,7 @@ const App = () => {
// Init the page settings and render
setExperienceSettings(settings);
})();
}, [isPreview, setExperienceSettings, setLoading]);
}, [isPreview, previewConfig, setExperienceSettings, setLoading]);
if (!experienceSettings) {
return null;