mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
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:
parent
e8b175c574
commit
dbe1c0fa2e
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>}
|
||||
|
@ -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} />
|
||||
|
@ -75,6 +75,7 @@ const ZapierModal = NiceModal.create(() => {
|
||||
testId='zapier-modal'
|
||||
title=''
|
||||
onOk={() => {
|
||||
updateRoute('integrations');
|
||||
modal.remove();
|
||||
}}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 */
|
||||
}
|
@ -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'},
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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&/);
|
||||
|
Loading…
Reference in New Issue
Block a user