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

Fixed bugs in AdminX portal and theme settings (#18099)

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

- Fixed portal preview when no tiers are enabled
- Fixed portal preview not respecting access setting
- Disabled portal customisation when nobody can sign up
- Fixed custom theme settings not updated when theme is changed
- Added publication icon setting in newsletters
- Added extremely rudimentary editor role display
- Fixed drag overlay position in modals
This commit is contained in:
Jono M 2023-09-13 08:10:33 +01:00 committed by GitHub
parent ed57df7ec6
commit 9b2387a364
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 105 additions and 51 deletions

View file

@ -1,7 +1,7 @@
import Button, {ButtonColor, ButtonProps} from '../Button'; import Button, {ButtonColor, ButtonProps} from '../Button';
import ButtonGroup from '../ButtonGroup'; import ButtonGroup from '../ButtonGroup';
import Heading from '../Heading'; import Heading from '../Heading';
import React, {useEffect} from 'react'; import React, {useEffect, useState} from 'react';
import StickyFooter from '../StickyFooter'; import StickyFooter from '../StickyFooter';
import clsx from 'clsx'; import clsx from 'clsx';
import useGlobalDirtyState from '../../../hooks/useGlobalDirtyState'; import useGlobalDirtyState from '../../../hooks/useGlobalDirtyState';
@ -68,6 +68,7 @@ const Modal: React.FC<ModalProps> = ({
}) => { }) => {
const modal = useModal(); const modal = useModal();
const {setGlobalDirtyState} = useGlobalDirtyState(); const {setGlobalDirtyState} = useGlobalDirtyState();
const [animationFinished, setAnimationFinished] = useState(false);
useEffect(() => { useEffect(() => {
setGlobalDirtyState(dirty); setGlobalDirtyState(dirty);
@ -95,6 +96,16 @@ const Modal: React.FC<ModalProps> = ({
}; };
}, [modal, dirty, afterClose, onCancel]); }, [modal, dirty, afterClose, onCancel]);
// The animation classes apply a transform to the modal, which breaks anything inside using position:fixed
// We should remove the class as soon as the animation is finished
useEffect(() => {
const timeout = setTimeout(() => {
setAnimationFinished(true);
}, 250);
return () => clearTimeout(timeout);
}, []);
let buttons: ButtonProps[] = []; let buttons: ButtonProps[] = [];
const removeModal = () => { const removeModal = () => {
@ -132,8 +143,8 @@ const Modal: React.FC<ModalProps> = ({
let modalClasses = clsx( let modalClasses = clsx(
'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden rounded bg-white dark:bg-black', 'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden rounded bg-white dark:bg-black',
formSheet ? 'shadow-md' : 'shadow-xl', formSheet ? 'shadow-md' : 'shadow-xl',
(animate && !formSheet) && 'animate-modal-in', (animate && !formSheet && !animationFinished) && 'animate-modal-in',
formSheet && 'animate-modal-in-reverse', (formSheet && !animationFinished) && 'animate-modal-in-reverse',
scrolling ? 'overflow-y-auto' : 'overflow-y-hidden' scrolling ? 'overflow-y-auto' : 'overflow-y-hidden'
); );

View file

@ -27,6 +27,8 @@ export interface CustomThemeSettingsResponseType {
const dataType = 'CustomThemeSettingsResponseType'; const dataType = 'CustomThemeSettingsResponseType';
export const customThemeSettingsDataType = dataType;
export const useBrowseCustomThemeSettings = createQuery<CustomThemeSettingsResponseType>({ export const useBrowseCustomThemeSettings = createQuery<CustomThemeSettingsResponseType>({
dataType, dataType,
path: '/custom_theme_settings/' path: '/custom_theme_settings/'

View file

@ -1,4 +1,5 @@
import {createMutation, createQuery} from '../utils/apiRequests'; import {createMutation, createQuery} from '../utils/apiRequests';
import {customThemeSettingsDataType} from './customThemeSettings';
// Types // Types
@ -65,6 +66,9 @@ export const useActivateTheme = createMutation<ThemesResponseType, string>({
} }
}) })
}) })
},
invalidateQueries: {
dataType: customThemeSettingsDataType
} }
}); });

View file

@ -78,6 +78,7 @@ export const useBrowseUsers = createQuery<UsersResponseType>({
export const useCurrentUser = createQuery<User>({ export const useCurrentUser = createQuery<User>({
dataType, dataType,
path: '/users/me/', path: '/users/me/',
defaultSearchParams: {include: 'roles'},
returnData: originalData => (originalData as UsersResponseType).users?.[0] returnData: originalData => (originalData as UsersResponseType).users?.[0]
}); });
@ -140,3 +141,7 @@ export function isOwnerUser(user: User) {
export function isAdminUser(user: User) { export function isAdminUser(user: User) {
return user.roles.some(role => role.name === 'Administrator'); return user.roles.some(role => role.name === 'Administrator');
} }
export function isEditorUser(user: User) {
return user.roles.some(role => role.name === 'Editor');
}

View file

@ -5,15 +5,24 @@ import EmailSettings from './settings/email/EmailSettings';
import GeneralSettings from './settings/general/GeneralSettings'; import GeneralSettings from './settings/general/GeneralSettings';
import MembershipSettings from './settings/membership/MembershipSettings'; import MembershipSettings from './settings/membership/MembershipSettings';
import SiteSettings from './settings/site/SiteSettings'; import SiteSettings from './settings/site/SiteSettings';
import Users from './settings/general/Users';
import {isEditorUser} from '../api/users';
import {useGlobalData} from './providers/GlobalDataProvider';
const Settings: React.FC = () => { const Settings: React.FC = () => {
const {currentUser} = useGlobalData();
return ( return (
<div className='mb-[40vh]'> <div className='mb-[40vh]'>
{isEditorUser(currentUser) ?
<Users keywords={[]} />
: <>
<GeneralSettings /> <GeneralSettings />
<SiteSettings /> <SiteSettings />
<MembershipSettings /> <MembershipSettings />
<EmailSettings /> <EmailSettings />
<AdvancedSettings /> <AdvancedSettings />
</>}
<div className='mt-40 text-sm'> <div className='mt-40 text-sm'>
<a className='text-green' href="/ghost/#/settings">Click here</a> to open the original Admin settings. <a className='text-green' href="/ghost/#/settings">Click here</a> to open the original Admin settings.
</div> </div>

View file

@ -6,6 +6,7 @@ import TextField from '../admin-x-ds/global/form/TextField';
import useFeatureFlag from '../hooks/useFeatureFlag'; import useFeatureFlag from '../hooks/useFeatureFlag';
import useRouting from '../hooks/useRouting'; import useRouting from '../hooks/useRouting';
import {getSettingValues} from '../api/settings'; import {getSettingValues} from '../api/settings';
import {isEditorUser} from '../api/users';
import {useGlobalData} from './providers/GlobalDataProvider'; import {useGlobalData} from './providers/GlobalDataProvider';
import {useSearch} from './providers/ServiceProvider'; import {useSearch} from './providers/ServiceProvider';
@ -13,7 +14,7 @@ const Sidebar: React.FC = () => {
const {filter, setFilter} = useSearch(); const {filter, setFilter} = useSearch();
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
const {settings, config} = useGlobalData(); const {settings, config, currentUser} = useGlobalData();
const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string]; const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string];
const handleSectionClick = (e: React.MouseEvent<HTMLButtonElement>) => { const handleSectionClick = (e: React.MouseEvent<HTMLButtonElement>) => {
@ -32,6 +33,11 @@ const Sidebar: React.FC = () => {
} }
}; };
// Editors can only see staff settings, so no point in showing navigation
if (isEditorUser(currentUser)) {
return null;
}
return ( return (
<div className='no-scrollbar tablet:h-[calc(100vh-5vmin-84px)] tablet:w-[240px] tablet:overflow-y-scroll'> <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 className='relative mb-10 md:pt-4 tablet:pt-[32px]'>

View file

@ -40,7 +40,7 @@ const Sidebar: React.FC<{
clearError: (field: string) => void; clearError: (field: string) => void;
}> = ({newsletter, updateNewsletter, validate, errors, clearError}) => { }> = ({newsletter, updateNewsletter, validate, errors, clearError}) => {
const {settings, siteData, config} = useGlobalData(); const {settings, siteData, config} = useGlobalData();
const [membersSupportAddress] = getSettingValues<string>(settings, ['members_support_address']); const [membersSupportAddress, icon] = getSettingValues<string>(settings, ['members_support_address', 'icon']);
const {mutateAsync: uploadImage} = useUploadImage(); const {mutateAsync: uploadImage} = useUploadImage();
const [selectedTab, setSelectedTab] = useState('generalSettings'); const [selectedTab, setSelectedTab] = useState('generalSettings');
const hasEmailCustomization = useFeatureFlag('emailCustomization'); const hasEmailCustomization = useFeatureFlag('emailCustomization');
@ -139,6 +139,13 @@ const Sidebar: React.FC<{
</div> </div>
</div> </div>
<ToggleGroup> <ToggleGroup>
{icon && <Toggle
checked={newsletter.show_header_icon}
direction="rtl"
label='Publication icon'
labelStyle='value'
onChange={e => updateNewsletter({show_header_icon: e.target.checked})}
/>}
<Toggle <Toggle
checked={newsletter.show_header_title} checked={newsletter.show_header_title}
direction="rtl" direction="rtl"

View file

@ -2,16 +2,22 @@ import Button from '../../../admin-x-ds/global/Button';
import React from 'react'; import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import useRouting from '../../../hooks/useRouting'; import useRouting from '../../../hooks/useRouting';
import {getSettingValues} from '../../../api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => { const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
const {settings} = useGlobalData();
const openPreviewModal = () => { const openPreviewModal = () => {
updateRoute('portal/edit'); updateRoute('portal/edit');
}; };
const [membersSignupAccess] = getSettingValues<string>(settings, ['members_signup_access']);
return ( return (
<SettingGroup <SettingGroup
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>} customButtons={<Button color='green' disabled={membersSignupAccess === 'none'} label='Customize' link onClick={openPreviewModal}/>}
description="Customize members modal signup flow" description="Customize members modal signup flow"
keywords={keywords} keywords={keywords}
navid='portal' navid='portal'

View file

@ -1,8 +1,9 @@
import React, {useEffect, useRef, useState} from 'react'; import React, {useEffect, useRef, useState} from 'react';
import useSettingGroup from '../../../../hooks/useSettingGroup'; import {Config} from '../../../../api/config';
import {Setting, getSettingValue} from '../../../../api/settings'; import {Setting, checkStripeEnabled, getSettingValue} from '../../../../api/settings';
import {SiteData} from '../../../../api/site'; import {SiteData} from '../../../../api/site';
import {Tier} from '../../../../api/tiers'; import {Tier} from '../../../../api/tiers';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
type PortalFrameProps = { type PortalFrameProps = {
settings: Setting[]; settings: Setting[];
@ -10,11 +11,12 @@ type PortalFrameProps = {
selectedTab: string; selectedTab: string;
} }
function getPortalPreviewUrl({settings, tiers, siteData, selectedTab}: { function getPortalPreviewUrl({settings, config, tiers, siteData, selectedTab}: {
settings: Setting[], settings: Setting[];
tiers: Tier[], config: Config;
siteData: SiteData|null, tiers: Tier[];
selectedTab: string siteData: SiteData | null;
selectedTab: string;
}) { }) {
if (!siteData?.url) { if (!siteData?.url) {
return null; return null;
@ -25,35 +27,25 @@ function getPortalPreviewUrl({settings, tiers, siteData, selectedTab}: {
const baseUrl = siteData.url.replace(/\/$/, ''); const baseUrl = siteData.url.replace(/\/$/, '');
const portalBase = '/?v=modal-portal-settings#/portal/preview'; const portalBase = '/?v=modal-portal-settings#/portal/preview';
const portalPlans: string[] = JSON.parse(getSettingValue<string>(settings, 'portal_plans') || '');
const membersSignupAccess = getSettingValue<string>(settings, 'members_signup_access');
const allowSelfSignup = membersSignupAccess === 'all' && (!checkStripeEnabled(settings, config) || portalPlans.includes('free'));
const settingsParam = new URLSearchParams(); const settingsParam = new URLSearchParams();
const signupButtonText = getSettingValue(settings, 'portal_button_signup_text') || ''; settingsParam.append('button', getSettingValue(settings, 'portal_button') ? 'true' : 'false');
let buttonIcon = getSettingValue(settings, 'portal_button_icon') as string || 'icon-1'; settingsParam.append('name', getSettingValue(settings, 'portal_name') ? 'true' : 'false');
const portalPlans: string[] = JSON.parse(getSettingValue(settings, 'portal_plans') as string); settingsParam.append('isFree', portalPlans.includes('free') ? 'true' : 'false');
const isFreeChecked = portalPlans.includes('free') ? 'true' : 'false'; settingsParam.append('isMonthly', checkStripeEnabled(settings, config) && portalPlans.includes('monthly') ? 'true' : 'false');
const isMonthlyChecked = portalPlans.includes('monthly') ? 'true' : 'false'; settingsParam.append('isYearly', checkStripeEnabled(settings, config) && portalPlans.includes('yearly') ? 'true' : 'false');
const isYearlyChecked = portalPlans.includes('yearly') ? 'true' : 'false'; settingsParam.append('page', selectedTab === 'account' ? 'accountHome' : 'signup');
const portalButton = getSettingValue(settings, 'portal_button') === true ? 'true' : 'false'; // Assuming a boolean settingsParam.append('buttonIcon', encodeURIComponent(getSettingValue(settings, 'portal_button_icon') || 'icon-1'));
const portalName = getSettingValue(settings, 'portal_name') as boolean; settingsParam.append('signupButtonText', encodeURIComponent(getSettingValue(settings, 'portal_button_signup_text') || ''));
const signupCheckboxRequired = getSettingValue(settings, 'portal_signup_checkbox_required') ? 'true' : 'false'; // Assuming a boolean settingsParam.append('membersSignupAccess', getSettingValue(settings, 'members_signup_access') || 'all');
const portalSignupTermsHtml = getSettingValue(settings, 'portal_signup_terms_html') || ''; settingsParam.append('allowSelfSignup', allowSelfSignup ? 'true' : 'false');
let page = selectedTab === 'account' ? 'accountHome' : 'signup'; settingsParam.append('signupTermsHtml', getSettingValue(settings, 'portal_signup_terms_html') || '');
settingsParam.append('signupCheckboxRequired', getSettingValue(settings, 'portal_signup_checkbox_required') ? 'true' : 'false');
settingsParam.append('button', portalButton);
settingsParam.append('name', portalName ? 'true' : 'false');
settingsParam.append('isFree', isFreeChecked);
settingsParam.append('isMonthly', isMonthlyChecked);
settingsParam.append('isYearly', isYearlyChecked);
settingsParam.append('page', page);
settingsParam.append('buttonIcon', encodeURIComponent(buttonIcon));
settingsParam.append('signupButtonText', encodeURIComponent(signupButtonText));
settingsParam.append('membersSignupAccess', 'all');
settingsParam.append('allowSelfSignup', 'true');
settingsParam.append('signupTermsHtml', portalSignupTermsHtml.toString());
settingsParam.append('signupCheckboxRequired', signupCheckboxRequired);
if (portalTiers && portalTiers.length) {
settingsParam.append('portalProducts', encodeURIComponent(portalTiers.join(','))); // assuming that it might be more than 1 settingsParam.append('portalProducts', encodeURIComponent(portalTiers.join(','))); // assuming that it might be more than 1
}
if (portalPlans && portalPlans.length) { if (portalPlans && portalPlans.length) {
settingsParam.append('portalPrices', encodeURIComponent(portalPlans.join(','))); settingsParam.append('portalPrices', encodeURIComponent(portalPlans.join(',')));
@ -76,14 +68,16 @@ function getPortalPreviewUrl({settings, tiers, siteData, selectedTab}: {
const PortalFrame: React.FC<PortalFrameProps> = ({settings, tiers, selectedTab}) => { const PortalFrame: React.FC<PortalFrameProps> = ({settings, tiers, selectedTab}) => {
const { const {
siteData siteData,
} = useSettingGroup(); config
} = useGlobalData();
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const [portalReady, setPortalReady] = useState(false); const [portalReady, setPortalReady] = useState(false);
let href = getPortalPreviewUrl({ let href = getPortalPreviewUrl({
settings, settings,
config,
tiers, tiers,
siteData, siteData,
selectedTab selectedTab

View file

@ -142,7 +142,8 @@ const PortalModal: React.FC = () => {
updateTier={updateTier} updateTier={updateTier}
/>; />;
const preview = <PortalPreview const preview = <PortalPreview
localSettings={formState.settings} localTiers={formState.tiers} localSettings={formState.settings}
localTiers={formState.tiers}
selectedTab={selectedPreviewTab} selectedTab={selectedPreviewTab}
/>; />;

View file

@ -51,7 +51,7 @@ export const globalDataRequests = {
browseSettings: {method: 'GET', path: /^\/settings\/\?group=/, response: responseFixtures.settings}, browseSettings: {method: 'GET', path: /^\/settings\/\?group=/, response: responseFixtures.settings},
browseConfig: {method: 'GET', path: '/config/', response: responseFixtures.config}, browseConfig: {method: 'GET', path: '/config/', response: responseFixtures.config},
browseSite: {method: 'GET', path: '/site/', response: responseFixtures.site}, browseSite: {method: 'GET', path: '/site/', response: responseFixtures.site},
browseMe: {method: 'GET', path: '/users/me/', response: responseFixtures.me} browseMe: {method: 'GET', path: '/users/me/?include=roles', response: responseFixtures.me}
}; };
export const limitRequests = { export const limitRequests = {

View file

@ -26,7 +26,16 @@
"milestone_notifications": true, "milestone_notifications": true,
"created_at": "2023-05-05T00:55:15.000Z", "created_at": "2023-05-05T00:55:15.000Z",
"updated_at": "2023-06-25T23:34:33.000Z", "updated_at": "2023-06-25T23:34:33.000Z",
"url": "http://localhost:2368/author/owner/" "url": "http://localhost:2368/author/owner/",
"roles": [
{
"id": "645453f3d254799990dd0e1a",
"name": "Owner",
"description": "Blog Owner",
"created_at": "2023-05-05T00:55:15.000Z",
"updated_at": "2023-05-05T00:55:15.000Z"
}
]
} }
] ]
} }