0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

AdminX UI fixes (#18110)

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

- fix tiers save & close bug
- show trial days on tier card
- removed 0's from monthly and yearly tier prices
- added color to learn more link in tier modal
- set background to white in newsletter preview
- fixed newsletter default sender data
- removed underline in "View in browser" link in newsletter preview
- updated copy in newsletters
- added Integrations' active indicator
- scrolling menu under searchbar to give search more prominance
- updated Portal modal buttons to be consistent with design settings
- fixed bug in AdminX loading Orb so that it actually starts auto-playing
This commit is contained in:
Peter Zimon 2023-09-14 13:04:31 +03:00 committed by GitHub
parent d7835ad5ed
commit dff8c38547
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 79 additions and 48 deletions

View file

@ -29,7 +29,7 @@ const queryClient = new QueryClient({
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false}: AppProps) {
const appClassName = clsx(
'admin-x-settings h-[100vh] w-full overflow-y-auto',
'admin-x-settings h-[100vh] w-full overflow-y-auto overflow-x-hidden',
darkMode && 'dark'
);

View file

@ -52,12 +52,11 @@ const MainContent: React.FC = () => {
<div className='-mx-6 h-[84px] bg-white px-6 tablet:m-0 tablet:bg-transparent tablet:p-0'>
<Heading>Settings</Heading>
</div>
<div className="relative mt-[-32px] w-full overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:hidden after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-[''] dark:after:from-black tablet:w-[260px] tablet:after:!visible tablet:after:!block">
<div className="relative mt-[-32px] w-full overflow-x-hidden">
<Sidebar />
</div>
</div>
<div className="relative flex-auto pt-[3vmin] tablet:ml-[300px] tablet:pt-[85px]">
{/* <div className='pointer-events-none fixed inset-x-0 top-0 z-[5] hidden h-[80px] bg-gradient-to-t from-transparent to-white to-60% dark:to-black tablet:!visible tablet:!block'></div> */}
<div className="relative flex-auto pt-[10vmin] tablet:ml-[300px] tablet:pt-[85px]">
<Settings />
</div>
</Page>

View file

@ -3,7 +3,7 @@ import TextField, {TextFieldProps} from './TextField';
import {currencyFromDecimal, currencyToDecimal} from '../../../utils/currency';
export type CurrencyFieldProps = Omit<TextFieldProps, 'type' | 'onChange' | 'value'> & {
valueInCents?: number;
valueInCents?: number | '';
currency?: string;
onChange?: (cents: number) => void;
}
@ -20,7 +20,7 @@ const CurrencyField: React.FC<CurrencyFieldProps> = ({
onChange,
...props
}) => {
const [localValue, setLocalValue] = useState(currencyToDecimal(valueInCents || 0).toString());
const [localValue, setLocalValue] = useState(valueInCents === '' ? '' : currencyToDecimal(valueInCents || 0).toString());
// While the user is editing we allow more lenient input, e.g. "1.32.566" to make it easier to type and change
const stripNonNumeric = (input: string) => input.replace(/[^\d.]+/g, '');

View file

@ -38,12 +38,12 @@ const Sidebar: React.FC = () => {
};
return (
<div className='no-scrollbar tablet:h-[calc(100vh-5vmin-84px)] tablet:w-[240px] tablet:overflow-y-scroll'>
<div className='relative mb-10 md:pt-4 tablet:pt-[32px]'>
<div>
<div className='relative md:pt-4 tablet:h-[64px] tablet:pt-[32px]'>
<Icon className='absolute top-2 md:top-6 tablet:top-10' colorClass='text-grey-500' name='magnifying-glass' size='sm' />
<TextField autoComplete="off" className='border-b border-grey-500 bg-transparent px-3 py-1.5 pl-[24px] text-sm dark:text-white' placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={updateSearch} />
</div>
<div className="hidden tablet:!visible tablet:!block">
<div className="no-scrollbar hidden pt-10 tablet:!visible tablet:!block tablet:h-[calc(100vh-5vmin-84px-64px)] tablet:w-[240px] tablet:overflow-y-auto">
<SettingNavSection keywords={Object.values(generalSearchKeywords).flat()} title="General">
<SettingNavItem keywords={generalSearchKeywords.titleAndDescription} navid='title-and-description' title="Title and description" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.timeZone} navid='timezone' title="Timezone" onClick={handleSectionClick} />

View file

@ -16,6 +16,7 @@ import {ReactComponent as PinturaIcon} from '../../../assets/icons/pintura.svg';
import {ReactComponent as SlackIcon} from '../../../assets/icons/slack.svg';
import {ReactComponent as UnsplashIcon} from '../../../assets/icons/unsplash.svg';
import {ReactComponent as ZapierIcon} from '../../../assets/icons/zapier.svg';
import {getSettingValues} from '../../../api/settings';
import {showToast} from '../../../admin-x-ds/global/Toast';
import {useGlobalData} from '../../providers/GlobalDataProvider';
@ -25,6 +26,7 @@ interface IntegrationItemProps {
detail: string,
action: () => void;
onDelete?: () => void;
active?: boolean;
disabled?: boolean;
testId?: string;
custom?: boolean;
@ -36,6 +38,7 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
detail,
action,
onDelete,
active,
disabled,
testId,
custom = false
@ -65,7 +68,7 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
detail={detail}
hideActions={!disabled}
testId={testId}
title={title}
title={active ? <span className='inline-flex items-center gap-1'>{title} <span className='inline-flex items-center rounded-full bg-[rgba(48,207,67,0.15)] px-1.5 py-0.5 text-2xs font-semibold uppercase tracking-wide text-green'>Active</span></span> : title}
onClick={handleClick}
/>;
};
@ -80,6 +83,9 @@ const BuiltInIntegrations: React.FC = () => {
const zapierDisabled = config.hostSettings?.limits?.customIntegrations?.disabled;
const {settings} = useGlobalData();
const [ampEnabled, unsplashEnabled, pinturaEnabled, firstPromoterEnabled, slackUrl, slackUsername] = getSettingValues<boolean>(settings, ['amp', 'unsplash', 'pintura', 'firstpromoter', 'slack_url', 'slack_username']);
return (
<List titleSeparator={false}>
<IntegrationItem
@ -96,6 +102,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => {
openModal('integrations/slack');
}}
active={slackUrl && slackUsername}
detail='A messaging app for teams'
icon={<SlackIcon className='h-8 w-8' />}
title='Slack' />
@ -104,6 +111,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => {
openModal('integrations/amp');
}}
active={ampEnabled}
detail='Google Accelerated Mobile Pages'
icon={<AmpIcon className='h-8 w-8' />}
title='AMP' />
@ -112,6 +120,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => {
openModal('integrations/unsplash');
}}
active={unsplashEnabled}
detail='Beautiful, free photos'
icon={<UnsplashIcon className='h-8 w-8' />}
title='Unsplash' />
@ -120,6 +129,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => {
openModal('integrations/firstpromoter');
}}
active={firstPromoterEnabled}
detail='Launch your member referral program'
icon={<FirstPromoterIcon className='h-8 w-8' />}
title='FirstPromoter' />
@ -128,6 +138,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => {
openModal('integrations/pintura');
}}
active={pinturaEnabled}
detail='Advanced image editing' icon=
{<PinturaIcon className='h-8 w-8' />} title
='Pintura' />

View file

@ -90,7 +90,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
} else {
showToast({
type: 'pageError',
message: 'Can\'t save integration, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save integration, please double check that you\'ve filled all mandatory fields.'
});
}
}}

View file

@ -43,7 +43,7 @@ const SlackModal = NiceModal.create(() => {
} else {
showToast({
type: 'pageError',
message: 'Can\'t save Slack settings, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save Slack settings, please double check that you\'ve filled all mandatory fields.'
});
}
};
@ -66,7 +66,7 @@ const SlackModal = NiceModal.create(() => {
} else {
showToast({
type: 'pageError',
message: 'Can\'t save Slack settings, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save Slack settings, please double check that you\'ve filled all mandatory fields.'
});
}
}}

View file

@ -67,7 +67,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
} else {
showToast({
type: 'pageError',
message: 'Can\'t save webhook, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save webhook, please double check that you\'ve filled all mandatory fields.'
});
}
}}

View file

@ -83,7 +83,7 @@ const AddNewsletterModal: React.FC<AddNewsletterModalProps> = () => {
} else {
showToast({
type: 'pageError',
message: 'Can\'t save newsletter, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.'
});
}
}}

View file

@ -20,6 +20,7 @@ import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import validator from 'validator';
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '../../../../api/newsletters';
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
@ -44,6 +45,8 @@ const Sidebar: React.FC<{
const {mutateAsync: uploadImage} = useUploadImage();
const [selectedTab, setSelectedTab] = useState('generalSettings');
const hasEmailCustomization = useFeatureFlag('emailCustomization');
const {localSettings} = useSettingGroup();
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
const replyToEmails = [
{label: `Newsletter address (${fullEmailAddress(newsletter.sender_email || 'noreply', siteData)})`, value: 'newsletter'},
@ -85,11 +88,11 @@ const Sidebar: React.FC<{
<TextArea rows={2} title="Description" value={newsletter.description || ''} onChange={e => updateNewsletter({description: e.target.value})} />
</Form>
<Form className='mt-6' gap='sm' margins='lg' title='Email addresses'>
<TextField placeholder="Ghost" title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
<TextField placeholder={siteTitle} title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
<TextField
error={Boolean(errors.sender_email)}
hint={errors.sender_email}
placeholder="noreply@localhost"
placeholder={fullEmailAddress(newsletter.sender_email || 'noreply', siteData)}
title="Sender email address"
value={newsletter.sender_email || ''}
onBlur={validate}
@ -143,21 +146,21 @@ const Sidebar: React.FC<{
checked={newsletter.show_header_icon}
direction="rtl"
label='Publication icon'
labelStyle='value'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_icon: e.target.checked})}
/>}
<Toggle
checked={newsletter.show_header_title}
direction="rtl"
label='Publication title'
labelStyle='value'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_title: e.target.checked})}
/>
<Toggle
checked={newsletter.show_header_name}
direction="rtl"
label='Newsletter name'
labelStyle='value'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_name: e.target.checked})}
/>
</ToggleGroup>
@ -432,7 +435,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter}> = ({newsl
} else {
showToast({
type: 'pageError',
message: 'Can\'t save newsletter, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.'
});
}
}}

View file

@ -93,6 +93,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
return <NewsletterPreviewContent
authorPlaceholder={currentUser.name || currentUser.email}
backgroundColor={colors.backgroundColor || '#ffffff'}
bodyFontCategory={newsletter.body_font_category}
footerContent={newsletter.footer_content}
headerIcon={newsletter.show_header_icon ? icon : undefined}

View file

@ -124,7 +124,7 @@ const NewsletterPreviewContent: React.FC<{
</span>
)}
</p>
<p className="pb-2 underline" style={{color: secondaryTextColor}}><span>View in browser</span></p>
<p className="pb-2" style={{color: secondaryTextColor}}><span>View in browser</span></p>
</div>
</div>
)}

View file

@ -782,7 +782,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
if (error) {
showToast({
type: 'pageError',
message: 'Can\'t save user, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save user, please double check that you\'ve filled all mandatory fields.'
});
setSaveState('');
return;

View file

@ -25,9 +25,9 @@ const AccountPage: React.FC<{
setValue(fullEmailAddress(settingValue, siteData!));
};
return <Form marginTop>
return <div className='mt-7'><Form>
<TextField title='Support email address' value={value} onBlur={updateSupportAddress} onChange={e => setValue(e.target.value)} />
</Form>;
</Form></div>;
};
export default AccountPage;

View file

@ -62,7 +62,7 @@ const LookAndFeel: React.FC<{
setUploadedIcon(undefined);
};
return <Form marginTop>
return <div className='mt-7'><Form>
<Toggle
checked={Boolean(portalButton)}
label='Show portal button'
@ -116,7 +116,7 @@ const LookAndFeel: React.FC<{
onChange={e => updateSetting('portal_button_signup_text', e.target.value)}
/>
}
</Form>;
</Form></div>;
};
export default LookAndFeel;

View file

@ -1,7 +1,7 @@
import AccountPage from './AccountPage';
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
import LookAndFeel from './LookAndFeel';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import NiceModal from '@ebay/nice-modal-react';
import PortalPreview from './PortalPreview';
import React, {useState} from 'react';
import SignupOptions from './SignupOptions';
@ -61,7 +61,6 @@ const Sidebar: React.FC<{
};
const PortalModal: React.FC = () => {
const modal = useModal();
const {updateRoute} = useRouting();
const [selectedPreviewTab, setSelectedPreviewTab] = useState('signup');
@ -152,7 +151,7 @@ const PortalModal: React.FC = () => {
{id: 'account', title: 'Account page'},
{id: 'links', title: 'Links'}
];
let okLabel = 'Save & close';
let okLabel = 'Save';
if (saveState === 'saving') {
okLabel = 'Saving...';
} else if (saveState === 'saved') {
@ -163,6 +162,7 @@ const PortalModal: React.FC = () => {
afterClose={() => {
updateRoute('portal');
}}
cancelLabel='Close'
deviceSelector={false}
dirty={saveState === 'unsaved'}
okLabel={okLabel}
@ -176,8 +176,6 @@ const PortalModal: React.FC = () => {
onOk={async () => {
if (!Object.values(errors).filter(Boolean).length) {
await handleSave();
updateRoute('portal');
modal.remove();
}
}}
onSelectURL={onSelectURL}

View file

@ -83,7 +83,7 @@ const SignupOptions: React.FC<{
});
}
return <Form marginTop>
return <div className='mt-7'><Form>
<Toggle
checked={Boolean(portalName)}
disabled={isDisabled}
@ -141,7 +141,7 @@ const SignupOptions: React.FC<{
labelStyle='heading'
onChange={e => updateSetting('portal_signup_checkbox_required', e.target.checked)}
/>}
</Form>;
</Form></div>;
};
export default SignupOptions;

View file

@ -121,11 +121,16 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
if (Object.values(validators).filter(validator => validator()).length) {
showToast({
type: 'pageError',
message: 'Can\'t save tier, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save tier, please double check that you\'ve filled all mandatory fields.'
});
return;
}
if (saveState !== 'unsaved') {
updateRoute('tiers');
modal.remove();
}
if (await handleSave()) {
updateRoute('tiers');
}
@ -173,7 +178,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
placeholder='1'
rightPlaceholder={`${formState.currency}/month`}
title='Monthly price'
valueInCents={formState.monthly_price || 0}
valueInCents={formState.monthly_price || ''}
hideTitle
onBlur={() => validators.monthly_price()}
onChange={price => updateForm(state => ({...state, monthly_price: price}))}
@ -184,7 +189,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
placeholder='10'
rightPlaceholder={`${formState.currency}/year`}
title='Yearly price'
valueInCents={formState.yearly_price || 0}
valueInCents={formState.yearly_price || ''}
hideTitle
onBlur={() => validators.yearly_price()}
onChange={price => updateForm(state => ({...state, yearly_price: price}))}
@ -197,9 +202,9 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
</div>
<TextField
disabled={!hasFreeTrial}
hint={<>
Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
</>}
hint={<div className='mt-1'>
Members will be subscribed at full price once the trial ends. <a className='text-green' href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
</div>}
placeholder='0'
rightPlaceholder='days'
title='Trial days'

View file

@ -2,6 +2,7 @@ import Button from '../../../../admin-x-ds/global/Button';
import Heading from '../../../../admin-x-ds/global/Heading';
import Icon from '../../../../admin-x-ds/global/Icon';
import React, {useState} from 'react';
import clsx from 'clsx';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import {TierFormState} from './TierDetailModal';
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
@ -13,12 +14,18 @@ interface TierDetailPreviewProps {
isFreeTier: boolean;
}
const TrialDaysLabel: React.FC<{trialDays: number}> = ({trialDays}) => {
export const TrialDaysLabel: React.FC<{size?: 'sm' | 'md'; trialDays: number;}> = ({size = 'md', trialDays}) => {
if (!trialDays) {
return null;
}
const containerClassName = clsx(
size === 'sm' ? 'px-1.5 py-0.5 text-xs' : 'px-2.5 py-1.5 text-sm',
'relative -mr-1 -mt-1 whitespace-nowrap rounded-full font-semibold leading-none tracking-wide text-grey-900'
);
return (
<span className="relative -mr-1 -mt-1 whitespace-nowrap rounded-full px-2.5 py-1.5 text-sm font-semibold leading-none tracking-wide text-grey-900">
<span className={containerClassName}>
<span className="absolute inset-0 block rounded-full bg-pink opacity-20"></span>
{trialDays} days free
</span>

View file

@ -4,6 +4,7 @@ import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
import React from 'react';
import useRouting from '../../../../hooks/useRouting';
import {Tier, useEditTier} from '../../../../api/tiers';
import {TrialDaysLabel} from './TierDetailPreview';
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
import {numberWithCommas} from '../../../../utils/helpers';
@ -35,6 +36,12 @@ const TierCard: React.FC<TierCardProps> = ({tier}) => {
<span className='text-xl font-bold tracking-tighter'>{numberWithCommas(currencyToDecimal(tier.monthly_price || 0))}</span>
{(tier.monthly_price && tier.monthly_price > 0) && <span className='text-sm text-grey-700'>/month</span>}
</div>
{tier.trial_days ?
<div className='mb-4 mt-1'>
<TrialDaysLabel size='sm' trialDays={tier.trial_days}/>
</div>
: ''
}
<div className='mt-2 line-clamp-2 text-[1.4rem] font-medium'>
{tier.description || <span className='opacity-50'>No description</span>}
</div>

View file

@ -136,7 +136,7 @@ const AddRecommendationModal: React.FC<AddRecommendationModalProps> = ({recommen
} else {
showToast({
type: 'pageError',
message: 'One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
message: 'One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
});
}
} catch (e) {

View file

@ -104,7 +104,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
} else {
showToast({
type: 'pageError',
message: 'One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
message: 'One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
});
}
}}

View file

@ -70,7 +70,7 @@ const EditRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({
} else {
showToast({
type: 'pageError',
message: 'One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
message: 'One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
});
}
}}

View file

@ -105,7 +105,7 @@ const useSettingGroup = ({onValidate}: {onValidate?: () => ErrorMessages} = {}):
} else {
showToast({
type: 'pageError',
message: 'Can\'t save settings! One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
message: 'Can\'t save settings! One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
});
}
return result;

View file

@ -297,12 +297,12 @@ export default class AdminXSettings extends Component {
justifyContent: 'center',
paddingBottom: '8vh'
}}>
<video width="100" height="100" loop autoplay muted playsinline preload="metadata" style={{
<video width="100" height="100" loop autoPlay muted playsInline preload="metadata" style={{
width: '100px',
height: '100px'
}}>
<source src="assets/videos/logo-loader.mp4" type="video/mp4" />
<div class="gh-loading-spinner"></div>
<div className="gh-loading-spinner"></div>
</video>
</div>
);