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 ButtonGroup from '../ButtonGroup';
import Heading from '../Heading';
import React, {useEffect} from 'react';
import React, {useEffect, useState} from 'react';
import StickyFooter from '../StickyFooter';
import clsx from 'clsx';
import useGlobalDirtyState from '../../../hooks/useGlobalDirtyState';
@ -68,6 +68,7 @@ const Modal: React.FC<ModalProps> = ({
}) => {
const modal = useModal();
const {setGlobalDirtyState} = useGlobalDirtyState();
const [animationFinished, setAnimationFinished] = useState(false);
useEffect(() => {
setGlobalDirtyState(dirty);
@ -95,6 +96,16 @@ const Modal: React.FC<ModalProps> = ({
};
}, [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[] = [];
const removeModal = () => {
@ -132,8 +143,8 @@ const Modal: React.FC<ModalProps> = ({
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',
formSheet ? 'shadow-md' : 'shadow-xl',
(animate && !formSheet) && 'animate-modal-in',
formSheet && 'animate-modal-in-reverse',
(animate && !formSheet && !animationFinished) && 'animate-modal-in',
(formSheet && !animationFinished) && 'animate-modal-in-reverse',
scrolling ? 'overflow-y-auto' : 'overflow-y-hidden'
);

View file

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

View file

@ -1,4 +1,5 @@
import {createMutation, createQuery} from '../utils/apiRequests';
import {customThemeSettingsDataType} from './customThemeSettings';
// 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>({
dataType,
path: '/users/me/',
defaultSearchParams: {include: 'roles'},
returnData: originalData => (originalData as UsersResponseType).users?.[0]
});
@ -140,3 +141,7 @@ export function isOwnerUser(user: User) {
export function isAdminUser(user: User) {
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 MembershipSettings from './settings/membership/MembershipSettings';
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 {currentUser} = useGlobalData();
return (
<div className='mb-[40vh]'>
<GeneralSettings />
<SiteSettings />
<MembershipSettings />
<EmailSettings />
<AdvancedSettings />
{isEditorUser(currentUser) ?
<Users keywords={[]} />
: <>
<GeneralSettings />
<SiteSettings />
<MembershipSettings />
<EmailSettings />
<AdvancedSettings />
</>}
<div className='mt-40 text-sm'>
<a className='text-green' href="/ghost/#/settings">Click here</a> to open the original Admin settings.
</div>

View file

@ -6,6 +6,7 @@ import TextField from '../admin-x-ds/global/form/TextField';
import useFeatureFlag from '../hooks/useFeatureFlag';
import useRouting from '../hooks/useRouting';
import {getSettingValues} from '../api/settings';
import {isEditorUser} from '../api/users';
import {useGlobalData} from './providers/GlobalDataProvider';
import {useSearch} from './providers/ServiceProvider';
@ -13,7 +14,7 @@ const Sidebar: React.FC = () => {
const {filter, setFilter} = useSearch();
const {updateRoute} = useRouting();
const {settings, config} = useGlobalData();
const {settings, config, currentUser} = useGlobalData();
const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string];
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 (
<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]'>

View file

@ -40,7 +40,7 @@ const Sidebar: React.FC<{
clearError: (field: string) => void;
}> = ({newsletter, updateNewsletter, validate, errors, clearError}) => {
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 [selectedTab, setSelectedTab] = useState('generalSettings');
const hasEmailCustomization = useFeatureFlag('emailCustomization');
@ -139,6 +139,13 @@ const Sidebar: React.FC<{
</div>
</div>
<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
checked={newsletter.show_header_title}
direction="rtl"

View file

@ -2,16 +2,22 @@ import Button from '../../../admin-x-ds/global/Button';
import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import useRouting from '../../../hooks/useRouting';
import {getSettingValues} from '../../../api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {updateRoute} = useRouting();
const {settings} = useGlobalData();
const openPreviewModal = () => {
updateRoute('portal/edit');
};
const [membersSignupAccess] = getSettingValues<string>(settings, ['members_signup_access']);
return (
<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"
keywords={keywords}
navid='portal'

View file

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

View file

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

View file

@ -51,7 +51,7 @@ export const globalDataRequests = {
browseSettings: {method: 'GET', path: /^\/settings\/\?group=/, response: responseFixtures.settings},
browseConfig: {method: 'GET', path: '/config/', response: responseFixtures.config},
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 = {

View file

@ -26,7 +26,16 @@
"milestone_notifications": true,
"created_at": "2023-05-05T00:55:15.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"
}
]
}
]
}