AdminX various UI fixes (#18089)

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

- some of the UI components' scrollbar was visible where it wasn't necessary
- metadata preview was shown in view mode too
- social accounts value was shown even if it was empty
- user detail modal was missing field descriptions
- it was not possible to reopen the Zapier modal after closing it the "Close" button
- copy had to be updated for analytics export to make it clear what is going to be exported
- invite modal had to be closed after successful invitation
- toggle component was only active on the text itself, instead of the whole row
This commit is contained in:
Peter Zimon 2023-09-12 17:11:12 +03:00 committed by GitHub
parent e8b175c574
commit dbe1c0fa2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 94 additions and 46 deletions

View File

@ -66,7 +66,7 @@ function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, d
</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='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> */}
<Settings />
</div>
</div>

View File

@ -40,7 +40,7 @@ function TabView<ID extends string = string>({
};
const containerClasses = clsx(
'flex w-full overflow-x-scroll',
'no-scrollbar flex w-full overflow-x-auto',
width === 'narrow' && 'gap-3',
width === 'normal' && 'gap-5',
width === 'wide' && 'gap-7',

View File

@ -67,7 +67,7 @@ const Table: React.FC<TableProps> = ({children, borderTop, hint, hintSeparator,
return (
<>
<div className='w-full overflow-x-scroll'>
<div className='w-full overflow-x-auto'>
{pageTitle && <Heading>{pageTitle}</Heading>}
{!isLoading && <table ref={table} className={tableClasses}>
<tbody>

View File

@ -63,7 +63,7 @@ const Form: React.FC<FormProps> = ({
}
let titleClasses = clsx(
grouped ? 'mb-2' : 'mb-3'
grouped ? 'mb-3' : 'mb-4'
);
if (grouped || title) {

View File

@ -89,7 +89,7 @@ const Toggle: React.FC<ToggleProps> = ({
type="checkbox"
onChange={onChange} />
{label &&
<label className={`flex flex-col hover:cursor-pointer ${direction === 'rtl' && 'order-1'} ${labelStyles}`} htmlFor={id}>
<label className={`flex grow flex-col hover:cursor-pointer ${direction === 'rtl' && 'order-1'} ${labelStyles}`} htmlFor={id}>
{
labelStyle === 'heading' ?
<span className={`${Heading6StylesGrey} mt-1`}>{label}</span>

View File

@ -1,15 +1,40 @@
import React from 'react';
import clsx from 'clsx';
interface ToggleGroupProps {
children?: React.ReactNode;
gap?: 'sm' | 'md' | 'lg';
className?: string;
}
/**
* A simple container to group sequencing toggle switches
*/
const ToggleGroup: React.FC<ToggleGroupProps> = ({children}) => {
const ToggleGroup: React.FC<ToggleGroupProps> = ({children, gap = 'md', className}) => {
let gapClass = 'gap-3';
switch (gap) {
case 'sm':
gapClass = 'gap-2';
break;
case 'md':
gapClass = 'gap-3';
break;
case 'lg':
gapClass = 'gap-4';
break;
default:
break;
}
className = clsx(
'flex flex-col gap-3',
gapClass,
className
);
return (
<div className='flex flex-col gap-3'>
<div className={className}>
{children}
</div>
);

View File

@ -161,7 +161,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
}
const containerClasses = clsx(
'min-w-100 absolute inset-y-0 left-0 right-[400px] flex grow flex-col overflow-y-scroll',
'min-w-100 absolute inset-y-0 left-0 right-[400px] flex grow flex-col overflow-y-auto',
previewBgClass
);

View File

@ -1,7 +1,7 @@
import React from 'react';
import SettingValue from './SettingValue';
import {ISettingValue} from './SettingValue';
import {SettingValueProps} from './SettingValue';
interface ISettingGroupContent {
columns?: 1 | 2;
@ -9,7 +9,7 @@ interface ISettingGroupContent {
/**
* Use this array to display setting values with standard formatting in the content area of a setting group
*/
values?: Array<ISettingValue>;
values?: Array<SettingValueProps>;
children?: React.ReactNode;
className?: string;
}

View File

@ -2,14 +2,19 @@ import React, {ReactNode} from 'react';
import Heading from '../global/Heading';
export interface ISettingValue {
key: string,
heading?: string,
value: ReactNode,
hint?: ReactNode
export interface SettingValueProps {
key: string;
heading?: string;
value: ReactNode;
hint?: ReactNode;
hideEmptyValue?: boolean;
}
const SettingValue: React.FC<ISettingValue> = ({heading, value, hint, ...props}) => {
const SettingValue: React.FC<SettingValueProps> = ({heading, value, hint, hideEmptyValue, ...props}) => {
if (!value && hideEmptyValue) {
return <></>;
}
return (
<div className='flex flex-col' {...props}>
{heading && <Heading grey={true} level={6}>{heading}</Heading>}

View File

@ -33,7 +33,7 @@ const Sidebar: React.FC = () => {
};
return (
<div className='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]'>
<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} />

View File

@ -75,6 +75,7 @@ const ZapierModal = NiceModal.create(() => {
testId='zapier-modal'
title=''
onOk={() => {
updateRoute('integrations');
modal.remove();
}}
>

View File

@ -271,39 +271,39 @@ const Sidebar: React.FC<{
checked={newsletter.show_feature_image}
direction="rtl"
label='Feature image'
labelStyle='value'
labelStyle='heading'
onChange={e => updateNewsletter({show_feature_image: e.target.checked})}
/>
</Form>
<Form className='mt-6' gap='sm' margins='lg' title='Footer'>
<ToggleGroup>
<ToggleGroup gap='lg'>
<Toggle
checked={newsletter.feedback_enabled}
direction="rtl"
label='Ask your readers for feedback'
labelStyle='value'
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='value'
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='value'
labelStyle='heading'
onChange={e => updateNewsletter({show_latest_posts: e.target.checked})}
/>
<Toggle
checked={newsletter.show_subscription_details}
direction="rtl"
label='Show subscription details'
labelStyle='value'
labelStyle='heading'
onChange={e => updateNewsletter({show_subscription_details: e.target.checked})}
/>
</ToggleGroup>

View File

@ -13,6 +13,7 @@ import {useEffect, useRef, useState} from 'react';
type RoleType = 'administrator' | 'editor' | 'author' | 'contributor';
const InviteUserModal = NiceModal.create(() => {
const modal = NiceModal.useModal();
const rolesQuery = useBrowseRoles();
const assignableRolesQuery = useBrowseRoles({
searchParams: {limit: 'all', permissions: 'assign'}
@ -108,6 +109,9 @@ const InviteUserModal = NiceModal.create(() => {
message: `Invitation successfully sent to ${email}`,
type: 'success'
});
modal.remove();
updateRoute('users');
} catch (e) {
setSaveState('error');

View File

@ -111,13 +111,17 @@ const Metadata: React.FC<{ keywords: string[] }> = ({keywords}) => {
onEditingChange={handleEditingChange}
onSave={handleSave}
>
<SearchEnginePreview
description={metaDescription ? metaDescription : siteDescription}
icon={siteData?.icon}
title={metaTitle ? metaTitle : siteTitle}
url={siteData?.url}
/>
{isEditing ? inputFields : null}
{isEditing &&
<>
<SearchEnginePreview
description={metaDescription ? metaDescription : siteDescription}
icon={siteData?.icon}
title={metaTitle ? metaTitle : siteTitle}
url={siteData?.url}
/>
{inputFields}
</>
}
</SettingGroup>
);
};

View File

@ -98,12 +98,14 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => {
{
heading: `URL of your publication's Facebook Page`,
key: 'facebook',
value: facebookUrl
value: facebookUrl,
hideEmptyValue: true
},
{
heading: 'URL of your TWITTER PROFILE',
key: 'twitter',
value: twitterUrl
value: twitterUrl,
hideEmptyValue: true
}
]}
/>

View File

@ -120,7 +120,7 @@ const BasicInputs: React.FC<UserDetailProps> = ({errors, validators, user, setUs
/>
<TextField
error={!!errors?.email}
hint={errors?.email || ''}
hint={errors?.email || 'Used for notifications'}
title="Email"
value={user.email}
onBlur={(e) => {
@ -159,6 +159,7 @@ const DetailsInputs: React.FC<UserDetailProps> = ({errors, validators, user, set
}}
/>
<TextField
hint="Where in the world do you live?"
title="Location"
value={user.location}
onChange={(e) => {
@ -167,7 +168,8 @@ const DetailsInputs: React.FC<UserDetailProps> = ({errors, validators, user, set
/>
<TextField
error={!!errors?.url}
hint={errors?.url || ''}
hint={errors?.url || 'Have a website or blog other than this one? Link it!'}
placeholder='https://example.com'
title="Website"
value={user.website}
onBlur={(e) => {
@ -178,6 +180,8 @@ const DetailsInputs: React.FC<UserDetailProps> = ({errors, validators, user, set
}}
/>
<TextField
hint='URL of your personal Facebook Profile'
placeholder='https://www.facebook.com/ghost'
title="Facebook profile"
value={user.facebook}
onChange={(e) => {
@ -185,6 +189,8 @@ const DetailsInputs: React.FC<UserDetailProps> = ({errors, validators, user, set
}}
/>
<TextField
hint='URL of your personal Twitter profile'
placeholder='https://twitter.com/ghost'
title="Twitter profile"
value={user.twitter}
onChange={(e) => {

View File

@ -106,7 +106,8 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
>
{inputs}
<div className='mt-1'>
<Button color='green' label='Export analytics' link={true} onClick={exportPosts} />
<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>
</SettingGroup>
);

View File

@ -73,4 +73,13 @@ html, body, #root {
.dark .gh-loading-orb {
filter: invert(100%);
}
.no-scrollbar::-webkit-scrollbar {
display: none; /* Chrome */
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

View File

@ -15,9 +15,6 @@ test.describe('Metadata settings', async () => {
const section = page.getByTestId('metadata');
await expect(section.getByText('Test Site')).toHaveCount(1);
await expect(section.getByText('Thoughts, stories and ideas.')).toHaveCount(1);
await section.getByRole('button', {name: 'Edit'}).click();
await section.getByLabel('Meta title').fill('Alternative title');
@ -27,9 +24,6 @@ test.describe('Metadata settings', async () => {
await expect(section.getByLabel('Meta title')).toHaveCount(0);
await expect(section.getByText('Alternative title')).toHaveCount(1);
await expect(section.getByText('Alternative description')).toHaveCount(1);
expect(lastApiRequests.editSettings?.body).toEqual({
settings: [
{key: 'meta_title', value: 'Alternative title'},

View File

@ -40,9 +40,6 @@ test.describe('User invitations', async () => {
await expect(page.getByTestId('toast')).toHaveText(/Invitation successfully sent to newuser@test\.com/);
// Currently clicking the backdrop is the only way to close this modal
await page.locator('#modal-backdrop').click({position: {x: 0, y: 0}});
await section.getByRole('tab', {name: 'Invited'}).click();
const listItem = section.getByTestId('user-invite').last();

View File

@ -49,7 +49,7 @@ test.describe('Analytics settings', async () => {
const section = page.getByTestId('analytics');
await section.getByRole('button', {name: 'Export analytics'}).click();
await section.getByRole('button', {name: 'Export post analytics'}).click();
const hasDownloadUrl = lastApiRequests.postsExport?.url?.includes('/posts/export/?limit=1000');
expect(hasDownloadUrl).toBe(true);

View File

@ -81,7 +81,7 @@ test.describe('Design settings', async () => {
await modal.getByLabel('Site description').fill('new description');
// set timeout of 500ms to wait for the debounce
await page.waitForTimeout(500);
await page.waitForTimeout(1000);
await modal.getByRole('button', {name: 'Save'}).click();
expect(lastPreviewRequest.previewHeader).toMatch(/&d=new\+description&/);