mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
AdminX UI fixes (#18167)
refs. https://github.com/TryGhost/Product/issues/3349
This commit is contained in:
parent
37a9ffc63e
commit
3043c27d5a
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>laptop</title><path d="M21,14.25V4.5A1.5,1.5,0,0,0,19.5,3H4.5A1.5,1.5,0,0,0,3,4.5v9.75Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><path d="M23.121,18.891A1.5,1.5,0,0,1,21.75,21H2.25A1.5,1.5,0,0,1,.879,18.891L3,14.25H21Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><line x1="10.5" y1="18" x2="13.5" y2="18" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>Desktop</title><path d="M21,14.25V4.5A1.5,1.5,0,0,0,19.5,3H4.5A1.5,1.5,0,0,0,3,4.5v9.75Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><path d="M23.121,18.891A1.5,1.5,0,0,1,21.75,21H2.25A1.5,1.5,0,0,1,.879,18.891L3,14.25H21Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><line x1="10.5" y1="18" x2="13.5" y2="18" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 636 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><rect x="5.25" y="0.75" width="13.5" height="22.5" rx="3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></rect><line x1="5.25" y1="17.75" x2="18.75" y2="17.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Mobile</title><g><rect x="5.25" y="0.75" width="13.5" height="22.5" rx="3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></rect><line x1="5.25" y1="17.75" x2="18.75" y2="17.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></g></svg>
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 419 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"><defs></defs><title>picture-sun</title><path d="M2.25.75h19.5s1.5 0 1.5 1.5v19.5s0 1.5-1.5 1.5H2.25s-1.5 0-1.5-1.5V2.25s0-1.5 1.5-1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M13.5 7.5a3 3 0 1 0 6 0 3 3 0 1 0-6 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3.961 14.959a8.194 8.194 0 0 1 11.694 4.149" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14.382 16.918a4.449 4.449 0 0 1 5.851-.125" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
After Width: | Height: | Size: 729 B |
@ -1,5 +1,6 @@
|
||||
import Icon from './Icon';
|
||||
import React, {HTMLProps} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './LoadingIndicator';
|
||||
|
||||
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
|
||||
@ -15,6 +16,7 @@ export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'label'
|
||||
color?: ButtonColor;
|
||||
fullWidth?: boolean;
|
||||
link?: boolean;
|
||||
linkWithPadding?: boolean;
|
||||
disabled?: boolean;
|
||||
unstyled?: boolean;
|
||||
className?: string;
|
||||
@ -34,6 +36,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
color = 'clear',
|
||||
fullWidth,
|
||||
link,
|
||||
linkWithPadding = false,
|
||||
disabled,
|
||||
unstyled = false,
|
||||
className = '',
|
||||
@ -47,50 +50,74 @@ const Button: React.FC<ButtonProps> = ({
|
||||
color = 'clear';
|
||||
}
|
||||
|
||||
let styles = '';
|
||||
|
||||
if (!unstyled) {
|
||||
styles += ' transition whitespace-nowrap flex items-center justify-center rounded-sm text-sm';
|
||||
styles += ((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? ' font-bold' : ' font-semibold';
|
||||
styles += !link ? `${size === 'sm' ? ' px-3 h-7 ' : ' px-4 h-[34px] '}` : '';
|
||||
className = clsx(
|
||||
'flex items-center justify-center whitespace-nowrap rounded-sm text-sm transition',
|
||||
((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? 'font-bold' : 'font-semibold',
|
||||
!link ? `${size === 'sm' ? ' h-7 px-3 ' : ' h-[34px] px-4 '}` : '',
|
||||
(link && linkWithPadding) && '-m-1 p-1',
|
||||
className
|
||||
);
|
||||
|
||||
switch (color) {
|
||||
case 'black':
|
||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`;
|
||||
className = clsx(
|
||||
link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`,
|
||||
className
|
||||
);
|
||||
loadingIndicatorColor = 'light';
|
||||
break;
|
||||
case 'grey':
|
||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`;
|
||||
className = clsx(
|
||||
link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`,
|
||||
className
|
||||
);
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
case 'green':
|
||||
styles += link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`;
|
||||
className = clsx(
|
||||
link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`,
|
||||
className
|
||||
);
|
||||
loadingIndicatorColor = 'light';
|
||||
break;
|
||||
case 'red':
|
||||
styles += link ? ' text-red hover:text-red-400' : ` bg-red text-white ${!disabled && 'hover:bg-red-400'}`;
|
||||
className = clsx(
|
||||
link ? 'text-red hover:text-red-400' : `bg-red text-white ${!disabled && 'hover:bg-red-400'}`,
|
||||
className
|
||||
);
|
||||
loadingIndicatorColor = 'light';
|
||||
break;
|
||||
case 'white':
|
||||
styles += link ? ' text-white hover:text-white dark:text-black dark:hover:text-grey-800' : ` bg-white dark:bg-black text-black dark:text-white`;
|
||||
className = clsx(
|
||||
link ? 'text-white hover:text-white dark:text-black dark:hover:text-grey-800' : `bg-white text-black dark:bg-black dark:text-white`,
|
||||
className
|
||||
);
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
case 'outline':
|
||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : `text-black border border-grey-300 bg-transparent dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`;
|
||||
className = clsx(
|
||||
link ? 'text-black hover:text-grey-800 dark:text-white' : `border border-grey-300 bg-transparent text-black dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`,
|
||||
className
|
||||
);
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
default:
|
||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`;
|
||||
className = clsx(
|
||||
link ? ' text-black hover:text-grey-800 dark:text-white' : ` text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`,
|
||||
className
|
||||
);
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
}
|
||||
|
||||
styles += (fullWidth && !link) ? ' w-full' : '';
|
||||
styles += (disabled) ? ' opacity-40' : ' cursor-pointer';
|
||||
className = clsx(
|
||||
(fullWidth && !link) && ' w-full',
|
||||
disabled ? 'opacity-40' : 'cursor-pointer',
|
||||
className
|
||||
);
|
||||
}
|
||||
|
||||
styles += ` ${className}`;
|
||||
|
||||
const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : '';
|
||||
|
||||
let labelClasses = '';
|
||||
@ -102,8 +129,8 @@ const Button: React.FC<ButtonProps> = ({
|
||||
<span className={labelClasses}>{label}</span>
|
||||
{loading && <div className='absolute flex'><LoadingIndicator color={loadingIndicatorColor} size={size}/><span className='sr-only'>Loading...</span></div>}
|
||||
</>;
|
||||
|
||||
const buttonElement = React.createElement(tag, {className: styles,
|
||||
|
||||
const buttonElement = React.createElement(tag, {className: className,
|
||||
disabled: disabled,
|
||||
type: 'button',
|
||||
onClick: onClick,
|
||||
|
@ -6,14 +6,15 @@ import {ButtonProps} from './Button';
|
||||
interface ButtonGroupProps {
|
||||
buttons: Array<ButtonProps>;
|
||||
link?: boolean;
|
||||
linkWithPadding?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link, className}) => {
|
||||
const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link, linkWithPadding, className}) => {
|
||||
return (
|
||||
<div className={`flex items-center ${link ? 'gap-5' : 'gap-3'} ${className}`}>
|
||||
{buttons.map(({key, ...props}) => (
|
||||
<Button key={key} link={link} {...props} />
|
||||
<Button key={key} link={link} linkWithPadding={linkWithPadding} {...props} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -6,8 +6,8 @@ interface DesktopChromeProps {
|
||||
|
||||
const DesktopChrome: React.FC<DesktopChromeProps & React.HTMLAttributes<HTMLDivElement>> = ({children, ...props}) => {
|
||||
return (
|
||||
<div className='flex h-full w-full flex-col px-5 pb-5' {...props}>
|
||||
<div className="h-full w-full overflow-hidden rounded-[4px] shadow-sm">
|
||||
<div className='flex h-full w-full flex-col px-5' {...props}>
|
||||
<div className="h-full w-full overflow-hidden rounded-t-[4px] shadow-sm">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -163,7 +163,7 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
|
||||
{customHeader ? customHeader :
|
||||
<SettingGroupHeader description={description} title={title!}>
|
||||
{customButtons ? customButtons :
|
||||
(onEditingChange && <ButtonGroup buttons={isEditing ? editButtons : viewButtons} link={true} />)}
|
||||
(onEditingChange && <ButtonGroup buttons={isEditing ? editButtons : viewButtons} link linkWithPadding />)}
|
||||
</SettingGroupHeader>
|
||||
}
|
||||
{children}
|
||||
|
@ -6,7 +6,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const SettingSectionHeader: React.FC<Props> = ({title, sticky = false}) => {
|
||||
let styles = 'pb-4 text-2xs font-semibold uppercase tracking-wider text-grey-700 z-10 ';
|
||||
let styles = 'pb-4 text-2xs font-semibold uppercase tracking-wider text-grey-700 z-20 ';
|
||||
if (sticky) {
|
||||
styles += ' sticky top-0 -mt-4 pt-4 bg-white dark:bg-black';
|
||||
}
|
||||
|
@ -63,8 +63,8 @@ const Sidebar: React.FC = () => {
|
||||
</SettingNavSection>
|
||||
|
||||
<SettingNavSection keywords={Object.values(membershipSearchKeywords).flat()} title="Membership">
|
||||
<SettingNavItem keywords={membershipSearchKeywords.portal} navid='portal' title="Portal" onClick={handleSectionClick} />
|
||||
<SettingNavItem keywords={membershipSearchKeywords.access} navid='access' title="Access" onClick={handleSectionClick} />
|
||||
<SettingNavItem keywords={membershipSearchKeywords.portal} navid='portal' title="Portal" onClick={handleSectionClick} />
|
||||
<SettingNavItem keywords={membershipSearchKeywords.tiers} navid='tiers' title="Tiers" onClick={handleSectionClick} />
|
||||
{hasTipsAndDonations && <SettingNavItem keywords={membershipSearchKeywords.tips} navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />}
|
||||
<SettingNavItem keywords={membershipSearchKeywords.embedSignupForm} navid='embed-signup-form' title="Embeddable signup form" onClick={handleSectionClick} />
|
||||
|
@ -11,7 +11,7 @@ const History: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='View history' link onClick={openHistoryModal}/>}
|
||||
customButtons={<Button color='green' label='View history' link linkWithPadding onClick={openHistoryModal}/>}
|
||||
description="View system event log"
|
||||
keywords={keywords}
|
||||
navid='history'
|
||||
|
@ -210,7 +210,7 @@ const Integrations: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
] as const;
|
||||
|
||||
const buttons = (
|
||||
<Button className='hidden md:!visible md:!block' color='green' label='Add custom integration' link={true} onClick={() => {
|
||||
<Button className='hidden md:!visible md:!block' color='green' label='Add custom integration' link linkWithPadding onClick={() => {
|
||||
updateRoute('integrations/add');
|
||||
setSelectedTab('custom');
|
||||
}} />
|
||||
|
@ -41,10 +41,10 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<SettingGroupHeader description='This is a testing ground for new or experimental features. They may change, break or inexplicably disappear at any time.' title='Labs' />
|
||||
{
|
||||
!isOpen ?
|
||||
<Button color='green' label='Open' link onClick={() => {
|
||||
<Button color='green' label='Open' link linkWithPadding onClick={() => {
|
||||
setIsOpen(true);
|
||||
}} /> :
|
||||
<Button color='green' label='Close' link onClick={() => {
|
||||
<Button color='green' label='Close' link linkWithPadding onClick={() => {
|
||||
setIsOpen(false);
|
||||
}} />
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import React, {ReactNode, useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@ -19,29 +20,28 @@ const APIKeyField: React.FC<APIKeyFieldProps> = ({label, text = '', hint, onRege
|
||||
};
|
||||
|
||||
const containerClasses = clsx(
|
||||
'group/api-keys relative mb-3 w-full overflow-hidden rounded py-1 text-sm hover:bg-grey-50 dark:hover:bg-black md:mb-0',
|
||||
label ? 'md:p-1' : 'md:px-0'
|
||||
'group/api-keys relative -mt-1 mb-1 w-full overflow-hidden border-b border-transparent py-2 hover:border-grey-300 dark:hover:border-grey-600'
|
||||
);
|
||||
|
||||
return <>
|
||||
{label && <div className='p-0 pr-4 text-sm text-grey-600 md:py-1'>{label}</div>}
|
||||
return <div className='mb-3 grid grid-cols-1'>
|
||||
{label && <Heading level={6} grey>{label}</Heading>}
|
||||
<div className={containerClasses}>
|
||||
{text}
|
||||
<span>{text}</span>
|
||||
{hint}
|
||||
<div className='visible absolute right-0 top-[50%] flex translate-y-[-50%] gap-1 bg-white pl-1 text-sm group-hover/api-keys:visible dark:bg-black md:invisible'>
|
||||
{onRegenerate && <Button color='outline' label='Regenerate' size='sm' onClick={onRegenerate} />}
|
||||
<Button color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyText} />
|
||||
<div className='visible absolute right-0 top-1/2 flex translate-y-[-50%] gap-1 pl-1 text-sm group-hover/api-keys:visible dark:bg-black md:invisible'>
|
||||
{onRegenerate && <Button className='!bg-white' color='outline' label='Regenerate' size='sm' onClick={onRegenerate} />}
|
||||
<Button className='!bg-white' color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyText} />
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
</div>;
|
||||
};
|
||||
|
||||
const APIKeys: React.FC<{hasLabel?: boolean; keys: APIKeyFieldProps[];}> = ({hasLabel = true, keys}) => {
|
||||
const APIKeys: React.FC<{hasLabel?: boolean; keys: APIKeyFieldProps[];}> = ({keys}) => {
|
||||
return (
|
||||
<div className={hasLabel ? 'grid grid-cols-1 md:grid-cols-[max-content_1fr]' : ''}>
|
||||
<div>
|
||||
{keys.map(key => <APIKeyField key={key.label} {...key} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default APIKeys;
|
||||
export default APIKeys;
|
@ -123,26 +123,24 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextField title='Description' value={formState.description || ''} onChange={e => updateForm(state => ({...state, description: e.target.value}))} />
|
||||
<div>
|
||||
<APIKeys keys={[
|
||||
{
|
||||
label: 'Content API key',
|
||||
text: contentApiKey?.secret,
|
||||
hint: contentKeyRegenerated ? <div className='text-green'>Content API Key was successfully regenerated</div> : undefined,
|
||||
onRegenerate: () => contentApiKey && handleRegenerate(contentApiKey, setContentKeyRegenerated)
|
||||
},
|
||||
{
|
||||
label: 'Admin API key',
|
||||
text: adminApiKey?.secret,
|
||||
hint: adminKeyRegenerated ? <div className='text-green'>Admin API Key was successfully regenerated</div> : undefined,
|
||||
onRegenerate: () => adminApiKey && handleRegenerate(adminApiKey, setAdminKeyRegenerated)
|
||||
},
|
||||
{
|
||||
label: 'API URL',
|
||||
text: window.location.origin + getGhostPaths().subdir
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
<APIKeys keys={[
|
||||
{
|
||||
label: 'Content API key',
|
||||
text: contentApiKey?.secret,
|
||||
hint: contentKeyRegenerated ? <div className='text-green'>Content API Key was successfully regenerated</div> : undefined,
|
||||
onRegenerate: () => contentApiKey && handleRegenerate(contentApiKey, setContentKeyRegenerated)
|
||||
},
|
||||
{
|
||||
label: 'Admin API key',
|
||||
text: adminApiKey?.secret,
|
||||
hint: adminKeyRegenerated ? <div className='text-green'>Admin API Key was successfully regenerated</div> : undefined,
|
||||
onRegenerate: () => adminApiKey && handleRegenerate(adminApiKey, setAdminKeyRegenerated)
|
||||
},
|
||||
{
|
||||
label: 'API URL',
|
||||
text: window.location.origin + getGhostPaths().subdir
|
||||
}
|
||||
]} />
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,11 +14,11 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
||||
extra
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex w-full flex-col gap-4 md:flex-row'>
|
||||
<div className='-mx-8 -mt-8 flex flex-col gap-4 bg-grey-75 p-8 md:flex-row'>
|
||||
<div className='h-14 w-14'>{icon}</div>
|
||||
<div className='flex min-w-0 flex-1 flex-col'>
|
||||
<div className='mt-1.5 flex min-w-0 flex-1 flex-col'>
|
||||
<h3>{title}</h3>
|
||||
<div className='text-grey-600'>{detail}</div>
|
||||
<div>{detail}</div>
|
||||
{extra && (
|
||||
<div className='mt-4'>{extra}</div>
|
||||
)}
|
||||
|
@ -70,10 +70,25 @@ const ZapierModal = NiceModal.create(() => {
|
||||
updateRoute('integrations');
|
||||
}}
|
||||
cancelLabel=''
|
||||
footer={
|
||||
<div className='mx-8 flex w-full items-center justify-between'>
|
||||
<a
|
||||
className='mt-1 self-baseline text-sm font-bold'
|
||||
href='https://zapier.com/apps/ghost/integrations?utm_medium=partner_api&utm_source=widget&utm_campaign=Widget'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'>
|
||||
View more Ghost integrations powered by <span><Logo className='relative top-[-2px] inline-block h-6' /></span>
|
||||
</a>
|
||||
<Button color='black' label='Close' onClick={() => {
|
||||
modal.remove();
|
||||
}} />
|
||||
</div>
|
||||
}
|
||||
okColor='black'
|
||||
okLabel='Close'
|
||||
testId='zapier-modal'
|
||||
title=''
|
||||
stickyFooter
|
||||
onOk={() => {
|
||||
updateRoute('integrations');
|
||||
modal.remove();
|
||||
@ -81,7 +96,7 @@ const ZapierModal = NiceModal.create(() => {
|
||||
>
|
||||
<IntegrationHeader
|
||||
detail='Automation for your favorite apps'
|
||||
extra={<APIKeys keys={[
|
||||
extra={<div className='-mb-4 mt-1'><APIKeys keys={[
|
||||
{
|
||||
label: 'Admin API key',
|
||||
text: adminApiKey?.secret,
|
||||
@ -89,17 +104,17 @@ const ZapierModal = NiceModal.create(() => {
|
||||
onRegenerate: handleRegenerate
|
||||
},
|
||||
{label: 'API URL', text: window.location.origin + getGhostPaths().subdir}
|
||||
]} />}
|
||||
]} /></div>}
|
||||
icon={<Icon className='h-14 w-14' />}
|
||||
title='Zapier'
|
||||
/>
|
||||
|
||||
<List className='mt-6'>
|
||||
<List className='-mb-8'>
|
||||
{zapierTemplates.map(template => (
|
||||
<ListItem
|
||||
action={<Button className='whitespace-nowrap text-sm font-semibold text-[#FF4A00]' href={template.url} label='Use this Zap' tag='a' target='_blank' link unstyled />}
|
||||
bgOnHover={false}
|
||||
className='flex items-center gap-3 py-2'
|
||||
className='flex items-center gap-3 py-2 pl-3'
|
||||
title={
|
||||
<div className='flex flex-col gap-4 md:flex-row md:items-center'>
|
||||
<div className='flex shrink-0 flex-nowrap items-center gap-2'>
|
||||
@ -114,16 +129,6 @@ const ZapierModal = NiceModal.create(() => {
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<div className='mt-6'>
|
||||
<a
|
||||
className='mt-6 self-baseline text-sm font-bold'
|
||||
href='https://zapier.com/apps/ghost/integrations?utm_medium=partner_api&utm_source=widget&utm_campaign=Widget'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'>
|
||||
View more Ghost integrations powered by <span><Logo className='relative top-[-2px] inline-block h-6' /></span>
|
||||
</a>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
@ -19,12 +19,12 @@ const EmailSettings: React.FC = () => {
|
||||
const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string];
|
||||
|
||||
return (
|
||||
<SettingSection keywords={Object.values(searchKeywords).flat()} title='Email newsletters'>
|
||||
<SettingSection keywords={Object.values(searchKeywords).flat()} title='Email newsletter'>
|
||||
<EnableNewsletters keywords={searchKeywords.enableNewsletters} />
|
||||
{newslettersEnabled !== 'disabled' && (
|
||||
<>
|
||||
<Newsletters keywords={searchKeywords.newsletters} />
|
||||
<DefaultRecipients keywords={searchKeywords.defaultRecipients} />
|
||||
<Newsletters keywords={searchKeywords.newsletters} />
|
||||
{!config.mailgunIsConfigured && <MailGun keywords={searchKeywords.mailgun} />}
|
||||
</>
|
||||
)}
|
||||
|
@ -15,7 +15,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const {data: {newsletters} = {}} = useBrowseNewsletters();
|
||||
|
||||
const buttons = (
|
||||
<Button color='green' label='Add newsletter' link={true} onClick={() => {
|
||||
<Button color='green' label='Add newsletter' link linkWithPadding onClick={() => {
|
||||
openNewsletterModal();
|
||||
}} />
|
||||
);
|
||||
|
@ -11,7 +11,7 @@ import NewsletterPreview from './NewsletterPreview';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useState} from 'react';
|
||||
import Select, {SelectOption} from '../../../../admin-x-ds/global/form/Select';
|
||||
import StickyFooter from '../../../../admin-x-ds/global/StickyFooter';
|
||||
import Separator from '../../../../admin-x-ds/global/Separator';
|
||||
import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
|
||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
@ -110,6 +110,26 @@ const Sidebar: React.FC<{
|
||||
onChange={e => updateNewsletter({subscribe_on_signup: e.target.checked})}
|
||||
/>
|
||||
</Form>
|
||||
<Separator />
|
||||
<div className='my-5 flex w-full items-start'>
|
||||
<span>
|
||||
<Icon className='mr-2 mt-[-1px]' colorClass='text-red' name='heart'/>
|
||||
</span>
|
||||
<Form marginBottom={false}>
|
||||
<Toggle
|
||||
checked={newsletter.show_badge}
|
||||
direction='rtl'
|
||||
label={
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<span className='text-sm md:text-base'>Promote independent publishing</span>
|
||||
<span className='text-[11px] leading-tight text-grey-700 md:text-xs md:leading-tight'>Show you’re a part of the indie publishing movement with a small badge in the footer</span>
|
||||
</div>
|
||||
}
|
||||
labelStyle='value'
|
||||
onChange={e => updateNewsletter({show_badge: e.target.checked})}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
},
|
||||
{
|
||||
@ -136,9 +156,9 @@ const Sidebar: React.FC<{
|
||||
updateNewsletter({header_image: imageUrl});
|
||||
}}
|
||||
>
|
||||
Upload header image
|
||||
<Icon colorClass='text-grey-700 dark:text-grey-300' name='picture' />
|
||||
</ImageUpload>
|
||||
<Hint>Optional, recommended size 1200x600</Hint>
|
||||
<Hint>1200x600, optional</Hint>
|
||||
</div>
|
||||
</div>
|
||||
<ToggleGroup>
|
||||
@ -146,21 +166,18 @@ const Sidebar: React.FC<{
|
||||
checked={newsletter.show_header_icon}
|
||||
direction="rtl"
|
||||
label='Publication icon'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_header_icon: e.target.checked})}
|
||||
/>}
|
||||
<Toggle
|
||||
checked={newsletter.show_header_title}
|
||||
direction="rtl"
|
||||
label='Publication title'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_header_title: e.target.checked})}
|
||||
/>
|
||||
<Toggle
|
||||
checked={newsletter.show_header_name}
|
||||
direction="rtl"
|
||||
label='Newsletter name'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_header_name: e.target.checked})}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
@ -292,28 +309,24 @@ const Sidebar: React.FC<{
|
||||
checked={newsletter.feedback_enabled}
|
||||
direction="rtl"
|
||||
label='Ask your readers for feedback'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({feedback_enabled: e.target.checked})}
|
||||
/>
|
||||
<Toggle
|
||||
checked={newsletter.show_comment_cta}
|
||||
direction="rtl"
|
||||
label='Add a link to your comments'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_comment_cta: e.target.checked})}
|
||||
/>
|
||||
<Toggle
|
||||
checked={newsletter.show_latest_posts}
|
||||
direction="rtl"
|
||||
label='Share your latest posts'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_latest_posts: e.target.checked})}
|
||||
/>
|
||||
<Toggle
|
||||
checked={newsletter.show_subscription_details}
|
||||
direction="rtl"
|
||||
label='Show subscription details'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_subscription_details: e.target.checked})}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
@ -336,31 +349,10 @@ const Sidebar: React.FC<{
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col justify-between'>
|
||||
<div className='flex flex-col'>
|
||||
<div className='px-7 pb-7 pt-5'>
|
||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={handleTabChange} />
|
||||
</div>
|
||||
<StickyFooter height={96}>
|
||||
<div className='flex w-full items-start px-7'>
|
||||
<span>
|
||||
<Icon className='mr-2 mt-[-1px]' colorClass='text-red' name='heart'/>
|
||||
</span>
|
||||
<Form marginBottom={false}>
|
||||
<Toggle
|
||||
checked={newsletter.show_badge}
|
||||
direction='rtl'
|
||||
label={
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<span className='text-sm md:text-base'>Promote independent publishing</span>
|
||||
<span className='text-[11px] leading-tight text-grey-700 md:text-xs md:leading-tight'>Show you’re a part of the indie publishing movement with a small badge in the footer</span>
|
||||
</div>
|
||||
}
|
||||
labelStyle='value'
|
||||
onChange={e => updateNewsletter({show_badge: e.target.checked})}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</StickyFooter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import TableRow from '../../../../admin-x-ds/global/TableRow';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
||||
import {Newsletter, useEditNewsletter} from '../../../../api/newsletters';
|
||||
import {numberWithCommas} from '../../../../utils/helpers';
|
||||
|
||||
interface NewslettersListProps {
|
||||
newsletters: Newsletter[]
|
||||
@ -21,7 +22,7 @@ const NewsletterItem: React.FC<{newsletter: Newsletter, onlyOne: boolean}> = ({n
|
||||
const limiter = useLimiter();
|
||||
|
||||
const action = newsletter.status === 'active' ? (
|
||||
<Button color='green' disabled={onlyOne} label='Archive' link onClick={() => {
|
||||
<Button color='red' disabled={onlyOne} label='Archive' link onClick={() => {
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Archive newsletter',
|
||||
prompt: <>
|
||||
@ -78,14 +79,14 @@ const NewsletterItem: React.FC<{newsletter: Newsletter, onlyOne: boolean}> = ({n
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{newsletter.count?.active_members}</span>
|
||||
<span>{numberWithCommas(newsletter.count?.active_members || 0) }</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>Subscribers</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{newsletter.count?.posts}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>Posts sent</span>
|
||||
<span>{numberWithCommas(newsletter.count?.posts || 0)}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>Delivered</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
@ -63,10 +63,10 @@ const TimeZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<SettingGroupContent values={[
|
||||
{
|
||||
key: 'site-timezone',
|
||||
value: publicationTimezone,
|
||||
hint: (
|
||||
<Hint timezone={publicationTimezone} />
|
||||
)
|
||||
value: <div className='flex flex-col'>
|
||||
{publicationTimezone}
|
||||
<span className='text-sm'><Hint timezone={publicationTimezone} /></span>
|
||||
</div>
|
||||
}
|
||||
]} />
|
||||
);
|
||||
|
@ -208,7 +208,7 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords,
|
||||
};
|
||||
|
||||
const buttons = (
|
||||
<Button color='green' label='Invite people' link={true} onClick={() => {
|
||||
<Button color='green' label='Invite people' link={true} linkWithPadding onClick={() => {
|
||||
showInviteModal();
|
||||
}} />
|
||||
);
|
||||
|
@ -53,8 +53,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<SettingGroupContent columns={2}>
|
||||
<Toggle
|
||||
checked={trackEmailOpens}
|
||||
direction='rtl'
|
||||
hint='Record when a member opens an email'
|
||||
label='Newsletter opens'
|
||||
onChange={(e) => {
|
||||
handleToggleChange('email_track_opens', e);
|
||||
@ -62,8 +60,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
/>
|
||||
<Toggle
|
||||
checked={trackEmailClicks}
|
||||
direction='rtl'
|
||||
hint='Record when a member clicks on any link in an email'
|
||||
label='Newsletter clicks'
|
||||
onChange={(e) => {
|
||||
handleToggleChange('email_track_clicks', e);
|
||||
@ -71,8 +67,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
/>
|
||||
<Toggle
|
||||
checked={trackMemberSources}
|
||||
direction='rtl'
|
||||
hint='Track the traffic sources and posts that drive the most member growth'
|
||||
label='Member sources'
|
||||
onChange={(e) => {
|
||||
handleToggleChange('members_track_sources', e);
|
||||
@ -80,8 +74,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
/>
|
||||
<Toggle
|
||||
checked={outboundLinkTagging}
|
||||
direction='rtl'
|
||||
hint='Make it easier for other sites to track the traffic you send them in their analytics'
|
||||
label='Outbound link tagging'
|
||||
onChange={(e) => {
|
||||
handleToggleChange('outbound_link_tagging', e);
|
||||
@ -93,21 +85,21 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
return (
|
||||
<SettingGroup
|
||||
description='Decide what data you collect from your members'
|
||||
hideEditButton={true}
|
||||
isEditing={isEditing}
|
||||
keywords={keywords}
|
||||
navid='analytics'
|
||||
saveState={saveState}
|
||||
testId='analytics'
|
||||
title='Analytics'
|
||||
hideEditButton
|
||||
onCancel={handleCancel}
|
||||
onEditingChange={handleEditingChange}
|
||||
onSave={handleSave}
|
||||
>
|
||||
{inputs}
|
||||
<div className='mt-1'>
|
||||
<Button color='green' label='Export post analytics' link={true} onClick={exportPosts} />
|
||||
<div className='text-xs text-grey-700 dark:text-grey-600'>Download the data from your last 1,000 posts</div>
|
||||
<div className='items-center-mt-1 flex justify-between'>
|
||||
<Button color='green' label='Export' link linkWithPadding onClick={exportPosts} />
|
||||
<a className='text-sm text-green' href="https://ghost.org/help/post-analytics/" rel="noopener noreferrer" target="_blank">Learn about analytics</a>
|
||||
</div>
|
||||
</SettingGroup>
|
||||
);
|
||||
|
@ -25,8 +25,8 @@ const MembershipSettings: React.FC = () => {
|
||||
|
||||
return (
|
||||
<SettingSection keywords={Object.values(searchKeywords).flat()} title='Membership'>
|
||||
<Portal keywords={searchKeywords.portal} />
|
||||
<Access keywords={searchKeywords.access} />
|
||||
<Portal keywords={searchKeywords.portal} />
|
||||
<Tiers keywords={searchKeywords.tiers} />
|
||||
{hasTipsAndDonations && <TipsOrDonations keywords={searchKeywords.tips} />}
|
||||
<EmbedSignupForm keywords={searchKeywords.embedSignupForm} />
|
||||
|
@ -17,7 +17,7 @@ const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' disabled={membersSignupAccess === 'none'} label='Customize' link onClick={openPreviewModal}/>}
|
||||
customButtons={<Button color='green' disabled={membersSignupAccess === 'none'} label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
|
||||
description="Customize members modal signup flow"
|
||||
keywords={keywords}
|
||||
navid='portal'
|
||||
|
@ -185,7 +185,7 @@ const Connected: React.FC<{onClose?: () => void}> = ({onClose}) => {
|
||||
return (
|
||||
<section>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Button className='dark:text-white' disabled={isFetchingMembers} icon='link-broken' iconColorClass='dark:text-white' label='Disconnect' link onClick={openDisconnectStripeModal} />
|
||||
<Button color='red' disabled={isFetchingMembers} icon='link-broken' iconColorClass='text-red' label='Disconnect' link onClick={openDisconnectStripeModal} />
|
||||
<Button icon='close' iconColorClass='dark:text-white' label='Close' size='sm' hideLabel link onClick={onClose} />
|
||||
</div>
|
||||
<div className='my-20 flex flex-col items-center'>
|
||||
|
@ -11,7 +11,7 @@ const AnnouncementBar: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Customize' link onClick={openModal}/>}
|
||||
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openModal}/>}
|
||||
description="Highlight important updates or offers"
|
||||
keywords={keywords}
|
||||
navid='announcement-bar'
|
||||
|
@ -11,7 +11,7 @@ const DesignSetting: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
|
||||
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
|
||||
description="Customize the look and feel of your site"
|
||||
keywords={keywords}
|
||||
navid='design'
|
||||
|
@ -11,7 +11,7 @@ const Navigation: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
|
||||
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
|
||||
description="Set up primary and secondary menus"
|
||||
keywords={keywords}
|
||||
navid='navigation'
|
||||
|
@ -64,12 +64,12 @@ const NavigationModal = NiceModal.create(() => {
|
||||
tabs={[
|
||||
{
|
||||
id: 'primary-nav',
|
||||
title: 'Primary navigation',
|
||||
title: 'Primary',
|
||||
contents: <NavigationEditForm baseUrl={siteData!.url} navigation={navigation} />
|
||||
},
|
||||
{
|
||||
id: 'secondary-nav',
|
||||
title: 'Secondary navigation',
|
||||
title: 'Secondary',
|
||||
contents: <NavigationEditForm baseUrl={siteData!.url} navigation={secondaryNavigation} />
|
||||
}
|
||||
]}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import Icon from '../../../../admin-x-ds/global/Icon';
|
||||
import NavigationItemEditor from './NavigationItemEditor';
|
||||
import React from 'react';
|
||||
import SortableList from '../../../../admin-x-ds/global/SortableList';
|
||||
@ -23,16 +24,19 @@ const NavigationEditForm: React.FC<{
|
||||
)}
|
||||
onMove={navigation.moveItem}
|
||||
/>
|
||||
<NavigationItemEditor
|
||||
action={<Button className='self-center' color='green' data-testid="add-button" icon="add" iconColorClass='text-white' size='sm' onClick={navigation.addItem} />}
|
||||
baseUrl={baseUrl}
|
||||
className="mt-1 pl-7"
|
||||
clearError={key => navigation.clearError(navigation.newItem.id, key)}
|
||||
data-testid="new-navigation-item"
|
||||
item={navigation.newItem}
|
||||
labelPlaceholder="New item label"
|
||||
updateItem={navigation.setNewItem}
|
||||
/>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Icon colorClass='text-grey-300 dark:text-grey-900 mt-1' name='add' size='sm' />
|
||||
<NavigationItemEditor
|
||||
action={<Button className='mx-2 self-center rounded bg-green p-1' data-testid="add-button" icon="add" iconColorClass='text-white' size='sm' unstyled onClick={navigation.addItem} />}
|
||||
baseUrl={baseUrl}
|
||||
className="mt-1"
|
||||
clearError={key => navigation.clearError(navigation.newItem.id, key)}
|
||||
data-testid="new-navigation-item"
|
||||
item={navigation.newItem}
|
||||
labelPlaceholder="New item label"
|
||||
updateItem={navigation.setNewItem}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -49,7 +49,7 @@ test.describe('Analytics settings', async () => {
|
||||
|
||||
const section = page.getByTestId('analytics');
|
||||
|
||||
await section.getByRole('button', {name: 'Export post analytics'}).click();
|
||||
await section.getByRole('button', {name: 'Export'}).click();
|
||||
|
||||
const hasDownloadUrl = lastApiRequests.postsExport?.url?.includes('/posts/export/?limit=1000');
|
||||
expect(hasDownloadUrl).toBe(true);
|
||||
|
Loading…
Reference in New Issue
Block a user