0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-04 02:01:58 -05:00

Improved Lexical behaviour in AdminX portal settings (#17232)

refs https://github.com/TryGhost/Product/issues/3545

- Fixed TODOs
- Fixed koenig editor not resizing as content expands
- Fixed placeholder in html field
- Updated signup options to validate terms length
This commit is contained in:
Jono M 2023-07-07 14:03:46 +12:00 committed by GitHub
parent fc7e150cc2
commit e397368393
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 15 deletions

View file

@ -91,7 +91,7 @@ const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
// ensure we're still showing errors in development
console.error(error); // eslint-disable-line
// TODO: Sentry integration?
// Pass down Sentry from Ember?
// if (this.config.sentry_dsn) {
// Sentry.captureException(error, {
// tags: {lexical: true},
@ -111,6 +111,12 @@ const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
}
}), [editor]);
const transformers = {
DEFAULT_NODES: koenig.DEFAULT_TRANSFORMERS,
BASIC_NODES: koenig.BASIC_TRANSFORMERS,
MINIMAL_NODES: koenig.MINIMAL_TRANSFORMERS
};
return (
<koenig.KoenigComposer
nodes={koenig[nodes || 'DEFAULT_NODES']}
@ -120,8 +126,7 @@ const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
className='koenig-lexical koenig-lexical-editor-input'
darkMode={false}
isSnippetsEnabled={false}
// TODO: set based on nodes or remove if there's another way to get the minimal editor
markdownTransformers={[]}
markdownTransformers={transformers[nodes || 'DEFAULT_NODES']}
placeholderClassName='koenig-lexical-editor-input-placeholder'
placeholderText={placeholder}
singleParagraph={true}
@ -146,7 +151,7 @@ const HtmlEditor: React.FC<HtmlEditorProps & {
editorVersion: config.editor.version
}), [config.editor.url, config.editor.version]);
return <div className={className || 'h-[200px] w-full'}>
return <div className={className || 'w-full'}>
<div className="koenig-react-editor w-full [&_*]:!font-inherit [&_*]:!text-inherit">
<ErrorHandler>
<Suspense fallback={<p className="koenig-react-editor-loading">Loading editor...</p>}>

View file

@ -28,7 +28,6 @@ const HtmlField: React.FC<HtmlFieldProps> = ({
title,
hideTitle,
error,
placeholder,
hint,
value,
clearBg = true,
@ -39,7 +38,7 @@ const HtmlField: React.FC<HtmlFieldProps> = ({
...props
}) => {
const textFieldClasses = unstyled ? '' : clsx(
'h-10 border-b py-2',
'min-h-10 border-b py-2',
clearBg ? 'bg-transparent' : 'bg-grey-75 px-[10px]',
error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`,
(title && !hideTitle && !clearBg) && `mt-2`,

View file

@ -16,14 +16,23 @@ const Sidebar: React.FC<{
updateSetting: (key: string, setting: SettingValue) => void
localTiers: Tier[]
updateTier: (tier: Tier) => void
}> = ({localSettings, updateSetting, localTiers, updateTier}) => {
errors: Record<string, string | undefined>
setError: (key: string, error: string | undefined) => void
}> = ({localSettings, updateSetting, localTiers, updateTier, errors, setError}) => {
const [selectedTab, setSelectedTab] = useState('signupOptions');
const tabs: Tab[] = [
{
id: 'signupOptions',
title: 'Signup options',
contents: <SignupOptions localSettings={localSettings} localTiers={localTiers} updateSetting={updateSetting} updateTier={updateTier} />
contents: <SignupOptions
errors={errors}
localSettings={localSettings}
localTiers={localTiers}
setError={setError}
updateSetting={updateSetting}
updateTier={updateTier}
/>
},
{
id: 'lookAndFeel',
@ -67,6 +76,8 @@ const PortalModal: React.FC = () => {
}
});
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
const updateSetting = (key: string, value: SettingValue) => {
updateForm(state => ({
...state,
@ -76,6 +87,13 @@ const PortalModal: React.FC = () => {
}));
};
const setError = (key: string, error: string | undefined) => {
setErrors(state => ({
...state,
[key]: error
}));
};
const updateTier = (newTier: Tier) => {
updateForm(state => ({
...state,
@ -89,7 +107,14 @@ const PortalModal: React.FC = () => {
setSelectedPreviewTab(id);
};
const sidebar = <Sidebar localSettings={formState.settings} localTiers={formState.tiers} updateSetting={updateSetting} updateTier={updateTier} />;
const sidebar = <Sidebar
errors={errors}
localSettings={formState.settings}
localTiers={formState.tiers}
setError={setError}
updateSetting={updateSetting}
updateTier={updateTier}
/>;
const preview = <PortalPreview
localSettings={formState.settings} localTiers={formState.tiers}
selectedTab={selectedPreviewTab}
@ -112,8 +137,10 @@ const PortalModal: React.FC = () => {
testId='portal-modal'
title='Portal'
onOk={async () => {
await handleSave();
modal.remove();
if (!Object.values(errors).filter(Boolean).length) {
await handleSave();
modal.remove();
}
}}
onSelectURL={onSelectURL}
/>;

View file

@ -1,7 +1,7 @@
import CheckboxGroup from '../../../../admin-x-ds/global/form/CheckboxGroup';
import Form from '../../../../admin-x-ds/global/form/Form';
import HtmlField from '../../../../admin-x-ds/global/form/HtmlField';
import React, {useContext, useMemo} from 'react';
import React, {useContext, useEffect, useMemo} from 'react';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import {CheckboxProps} from '../../../../admin-x-ds/global/form/Checkbox';
import {Setting, SettingValue, Tier} from '../../../../types/api';
@ -13,7 +13,9 @@ const SignupOptions: React.FC<{
updateSetting: (key: string, setting: SettingValue) => void
localTiers: Tier[]
updateTier: (tier: Tier) => void
}> = ({localSettings, updateSetting, localTiers, updateTier}) => {
errors: Record<string, string | undefined>
setError: (key: string, error: string | undefined) => void
}> = ({localSettings, updateSetting, localTiers, updateTier, errors, setError}) => {
const {config} = useContext(SettingsContext);
const [membersSignupAccess, portalName, portalSignupTermsHtml, portalSignupCheckboxRequired, portalPlansJson] = getSettingValues(
@ -21,12 +23,21 @@ const SignupOptions: React.FC<{
);
const portalPlans = JSON.parse(portalPlansJson?.toString() || '[]') as string[];
const signupTermsMaxLength = 115;
const signupTermsLength = useMemo(() => {
const div = document.createElement('div');
div.innerHTML = portalSignupTermsHtml?.toString() || '';
return div.innerText.length;
}, [portalSignupTermsHtml]);
useEffect(() => {
if (signupTermsLength > signupTermsMaxLength) {
setError('portal_signup_terms_html', 'Signup notice is too long');
} else {
setError('portal_signup_terms_html', undefined);
}
}, [signupTermsLength, setError]);
const togglePlan = (plan: string) => {
const index = portalPlans.indexOf(plan);
@ -107,11 +118,12 @@ const SignupOptions: React.FC<{
/>
)}
{/* TODO: validate length according to hint */}
<HtmlField
config={config as { editor: any }}
hint={<>Recommended: <strong>115</strong> characters. You've used <strong className="text-green">{signupTermsLength}</strong></>}
error={Boolean(errors.portal_signup_terms_html)}
hint={errors.portal_signup_terms_html || <>Recommended: <strong>115</strong> characters. You've used <strong className="text-green">{signupTermsLength}</strong></>}
nodes='MINIMAL_NODES'
placeholder={`By signing up, I agree to receive emails from ...`}
title='Display notice at signup'
value={portalSignupTermsHtml?.toString()}
onChange={html => updateSetting('portal_signup_terms_html', html)}