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:
parent
ed57df7ec6
commit
9b2387a364
12 changed files with 105 additions and 51 deletions
|
@ -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'
|
||||
);
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ export interface CustomThemeSettingsResponseType {
|
|||
|
||||
const dataType = 'CustomThemeSettingsResponseType';
|
||||
|
||||
export const customThemeSettingsDataType = dataType;
|
||||
|
||||
export const useBrowseCustomThemeSettings = createQuery<CustomThemeSettingsResponseType>({
|
||||
dataType,
|
||||
path: '/custom_theme_settings/'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]'>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>;
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue