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:
parent
5784cfeb18
commit
268679b02e
15 changed files with 137 additions and 7 deletions
8
.changeset-staged/quick-schools-complain.md
Normal file
8
.changeset-staged/quick-schools-complain.md
Normal 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.
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
.customCssCodeEditor {
|
||||
max-height: calc(100vh - 260px);
|
||||
min-height: 111px; // min-height to show three lines of code
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: '휴대전화번호',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: '手机号码',
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue