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:
parent
fc7e150cc2
commit
e397368393
4 changed files with 58 additions and 15 deletions
|
@ -91,7 +91,7 @@ const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
|
||||||
// ensure we're still showing errors in development
|
// ensure we're still showing errors in development
|
||||||
console.error(error); // eslint-disable-line
|
console.error(error); // eslint-disable-line
|
||||||
|
|
||||||
// TODO: Sentry integration?
|
// Pass down Sentry from Ember?
|
||||||
// if (this.config.sentry_dsn) {
|
// if (this.config.sentry_dsn) {
|
||||||
// Sentry.captureException(error, {
|
// Sentry.captureException(error, {
|
||||||
// tags: {lexical: true},
|
// tags: {lexical: true},
|
||||||
|
@ -111,6 +111,12 @@ const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
|
||||||
}
|
}
|
||||||
}), [editor]);
|
}), [editor]);
|
||||||
|
|
||||||
|
const transformers = {
|
||||||
|
DEFAULT_NODES: koenig.DEFAULT_TRANSFORMERS,
|
||||||
|
BASIC_NODES: koenig.BASIC_TRANSFORMERS,
|
||||||
|
MINIMAL_NODES: koenig.MINIMAL_TRANSFORMERS
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<koenig.KoenigComposer
|
<koenig.KoenigComposer
|
||||||
nodes={koenig[nodes || 'DEFAULT_NODES']}
|
nodes={koenig[nodes || 'DEFAULT_NODES']}
|
||||||
|
@ -120,8 +126,7 @@ const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
|
||||||
className='koenig-lexical koenig-lexical-editor-input'
|
className='koenig-lexical koenig-lexical-editor-input'
|
||||||
darkMode={false}
|
darkMode={false}
|
||||||
isSnippetsEnabled={false}
|
isSnippetsEnabled={false}
|
||||||
// TODO: set based on nodes or remove if there's another way to get the minimal editor
|
markdownTransformers={transformers[nodes || 'DEFAULT_NODES']}
|
||||||
markdownTransformers={[]}
|
|
||||||
placeholderClassName='koenig-lexical-editor-input-placeholder'
|
placeholderClassName='koenig-lexical-editor-input-placeholder'
|
||||||
placeholderText={placeholder}
|
placeholderText={placeholder}
|
||||||
singleParagraph={true}
|
singleParagraph={true}
|
||||||
|
@ -146,7 +151,7 @@ const HtmlEditor: React.FC<HtmlEditorProps & {
|
||||||
editorVersion: config.editor.version
|
editorVersion: config.editor.version
|
||||||
}), [config.editor.url, 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">
|
<div className="koenig-react-editor w-full [&_*]:!font-inherit [&_*]:!text-inherit">
|
||||||
<ErrorHandler>
|
<ErrorHandler>
|
||||||
<Suspense fallback={<p className="koenig-react-editor-loading">Loading editor...</p>}>
|
<Suspense fallback={<p className="koenig-react-editor-loading">Loading editor...</p>}>
|
||||||
|
|
|
@ -28,7 +28,6 @@ const HtmlField: React.FC<HtmlFieldProps> = ({
|
||||||
title,
|
title,
|
||||||
hideTitle,
|
hideTitle,
|
||||||
error,
|
error,
|
||||||
placeholder,
|
|
||||||
hint,
|
hint,
|
||||||
value,
|
value,
|
||||||
clearBg = true,
|
clearBg = true,
|
||||||
|
@ -39,7 +38,7 @@ const HtmlField: React.FC<HtmlFieldProps> = ({
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const textFieldClasses = unstyled ? '' : clsx(
|
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]',
|
clearBg ? 'bg-transparent' : 'bg-grey-75 px-[10px]',
|
||||||
error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`,
|
error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`,
|
||||||
(title && !hideTitle && !clearBg) && `mt-2`,
|
(title && !hideTitle && !clearBg) && `mt-2`,
|
||||||
|
|
|
@ -16,14 +16,23 @@ const Sidebar: React.FC<{
|
||||||
updateSetting: (key: string, setting: SettingValue) => void
|
updateSetting: (key: string, setting: SettingValue) => void
|
||||||
localTiers: Tier[]
|
localTiers: Tier[]
|
||||||
updateTier: (tier: Tier) => void
|
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 [selectedTab, setSelectedTab] = useState('signupOptions');
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
{
|
{
|
||||||
id: 'signupOptions',
|
id: 'signupOptions',
|
||||||
title: 'Signup options',
|
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',
|
id: 'lookAndFeel',
|
||||||
|
@ -67,6 +76,8 @@ const PortalModal: React.FC = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
||||||
|
|
||||||
const updateSetting = (key: string, value: SettingValue) => {
|
const updateSetting = (key: string, value: SettingValue) => {
|
||||||
updateForm(state => ({
|
updateForm(state => ({
|
||||||
...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) => {
|
const updateTier = (newTier: Tier) => {
|
||||||
updateForm(state => ({
|
updateForm(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -89,7 +107,14 @@ const PortalModal: React.FC = () => {
|
||||||
setSelectedPreviewTab(id);
|
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
|
const preview = <PortalPreview
|
||||||
localSettings={formState.settings} localTiers={formState.tiers}
|
localSettings={formState.settings} localTiers={formState.tiers}
|
||||||
selectedTab={selectedPreviewTab}
|
selectedTab={selectedPreviewTab}
|
||||||
|
@ -112,8 +137,10 @@ const PortalModal: React.FC = () => {
|
||||||
testId='portal-modal'
|
testId='portal-modal'
|
||||||
title='Portal'
|
title='Portal'
|
||||||
onOk={async () => {
|
onOk={async () => {
|
||||||
await handleSave();
|
if (!Object.values(errors).filter(Boolean).length) {
|
||||||
modal.remove();
|
await handleSave();
|
||||||
|
modal.remove();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onSelectURL={onSelectURL}
|
onSelectURL={onSelectURL}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import CheckboxGroup from '../../../../admin-x-ds/global/form/CheckboxGroup';
|
import CheckboxGroup from '../../../../admin-x-ds/global/form/CheckboxGroup';
|
||||||
import Form from '../../../../admin-x-ds/global/form/Form';
|
import Form from '../../../../admin-x-ds/global/form/Form';
|
||||||
import HtmlField from '../../../../admin-x-ds/global/form/HtmlField';
|
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 Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import {CheckboxProps} from '../../../../admin-x-ds/global/form/Checkbox';
|
import {CheckboxProps} from '../../../../admin-x-ds/global/form/Checkbox';
|
||||||
import {Setting, SettingValue, Tier} from '../../../../types/api';
|
import {Setting, SettingValue, Tier} from '../../../../types/api';
|
||||||
|
@ -13,7 +13,9 @@ const SignupOptions: React.FC<{
|
||||||
updateSetting: (key: string, setting: SettingValue) => void
|
updateSetting: (key: string, setting: SettingValue) => void
|
||||||
localTiers: Tier[]
|
localTiers: Tier[]
|
||||||
updateTier: (tier: Tier) => void
|
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 {config} = useContext(SettingsContext);
|
||||||
|
|
||||||
const [membersSignupAccess, portalName, portalSignupTermsHtml, portalSignupCheckboxRequired, portalPlansJson] = getSettingValues(
|
const [membersSignupAccess, portalName, portalSignupTermsHtml, portalSignupCheckboxRequired, portalPlansJson] = getSettingValues(
|
||||||
|
@ -21,12 +23,21 @@ const SignupOptions: React.FC<{
|
||||||
);
|
);
|
||||||
const portalPlans = JSON.parse(portalPlansJson?.toString() || '[]') as string[];
|
const portalPlans = JSON.parse(portalPlansJson?.toString() || '[]') as string[];
|
||||||
|
|
||||||
|
const signupTermsMaxLength = 115;
|
||||||
const signupTermsLength = useMemo(() => {
|
const signupTermsLength = useMemo(() => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerHTML = portalSignupTermsHtml?.toString() || '';
|
div.innerHTML = portalSignupTermsHtml?.toString() || '';
|
||||||
return div.innerText.length;
|
return div.innerText.length;
|
||||||
}, [portalSignupTermsHtml]);
|
}, [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 togglePlan = (plan: string) => {
|
||||||
const index = portalPlans.indexOf(plan);
|
const index = portalPlans.indexOf(plan);
|
||||||
|
|
||||||
|
@ -107,11 +118,12 @@ const SignupOptions: React.FC<{
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TODO: validate length according to hint */}
|
|
||||||
<HtmlField
|
<HtmlField
|
||||||
config={config as { editor: any }}
|
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'
|
nodes='MINIMAL_NODES'
|
||||||
|
placeholder={`By signing up, I agree to receive emails from ...`}
|
||||||
title='Display notice at signup'
|
title='Display notice at signup'
|
||||||
value={portalSignupTermsHtml?.toString()}
|
value={portalSignupTermsHtml?.toString()}
|
||||||
onChange={html => updateSetting('portal_signup_terms_html', html)}
|
onChange={html => updateSetting('portal_signup_terms_html', html)}
|
||||||
|
|
Loading…
Add table
Reference in a new issue