0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Refactor user detail modal in AdminX (#18414)

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

- the User detail modal class structure was way overcomplicated
- the top part of the outline highlight of a setting group which is first in a section was cut
- toggle label style was inconsisten in Newsletter settings
- Audience feedback was not enabled by default when creating a Newsletter
- the whole UI was using Twitter, instead of "X"
This commit is contained in:
Peter Zimon 2023-10-02 15:14:46 +03:00 committed by GitHub
parent f70dcd6861
commit b490057429
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 92 additions and 93 deletions

View file

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" aria-hidden="true" class="r-18jsvk2 r-4qtqp9 r-yyyyoo r-rxcuwo r-1777fci r-m327ed r-dnmrzs r-494qqr r-bnwqim r-1plcrui r-lrvibr"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg>

After

Width:  |  Height:  |  Size: 340 B

View file

@ -57,7 +57,7 @@ const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> =
if (!useLabelTag) {
switch (level) {
case 1:
styles += ' md:text-5xl leading-tight';
styles += ' md:text-5xl leading-tighter';
break;
case 2:
styles += ' md:text-3xl';

View file

@ -6,7 +6,7 @@ interface Props {
}
const SettingSectionHeader: React.FC<Props> = ({title, sticky = false}) => {
let styles = 'pb-[10px] text-2xs font-semibold uppercase tracking-wider text-grey-700 z-20 ';
let styles = 'pb-[9px] mb-px 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';
}

View file

@ -79,7 +79,7 @@ const Sidebar: React.FC = () => {
<SettingNavItem keywords={generalSearchKeywords.timeZone} navid='timezone' title="Timezone" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.publicationLanguage} navid='publication-language' title="Publication language" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.metadata} navid='metadata' title="Meta data" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.twitter} navid='twitter' title="Twitter card" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.twitter} navid='twitter' title="X card" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.facebook} navid='facebook' title="Facebook card" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.socialAccounts} navid='social-accounts' title="Social accounts" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.lockSite} navid='locksite' title="Make this site private" onClick={handleSectionClick} />

View file

@ -36,7 +36,8 @@ const AddNewsletterModal: React.FC<RoutingModalProps> = () => {
const response = await addNewsletter({
name: formState.name,
description: formState.description,
opt_in_existing: formState.optInExistingSubscribers
opt_in_existing: formState.optInExistingSubscribers,
feedback_enabled: true
});
updateRoute({route: `newsletters/show/${response.newsletters[0].id}`});

View file

@ -373,7 +373,6 @@ const Sidebar: React.FC<{
checked={newsletter.show_feature_image}
direction="rtl"
label='Feature image'
labelStyle='heading'
onChange={e => updateNewsletter({show_feature_image: e.target.checked})}
/>
</Form>

View file

@ -16,7 +16,7 @@ export const searchKeywords = {
timeZone: ['time', 'date', 'site timezone', 'time zone'],
publicationLanguage: ['publication language', 'locale'],
metadata: ['metadata', 'title', 'description', 'search', 'engine', 'google'],
twitter: ['twitter card', 'structured data', 'rich cards'],
twitter: ['twitter card', 'structured data', 'rich cards', 'x'],
facebook: ['facebook card', 'structured data', 'rich cards'],
socialAccounts: ['social accounts', 'facebook', 'twitter', 'structured data', 'rich cards'],
lockSite: ['private', 'password', 'lock site'],

View file

@ -103,7 +103,7 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => {
hideEmptyValue: true
},
{
heading: 'URL of your TWITTER PROFILE',
heading: 'URL of your X (formerly Twitter) profile',
key: 'twitter',
value: twitterUrl,
hideEmptyValue: true
@ -139,7 +139,7 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => {
hint={errors.twitter}
inputRef={twitterInputRef}
placeholder="https://twitter.com/ghost"
title="URL of your Twitter profile"
title="URL of your X (formerly Twitter) profile"
value={twitterUrl}
onBlur={(e) => {
try {

View file

@ -6,7 +6,7 @@ import TextField from '../../../admin-x-ds/global/form/TextField';
import useHandleError from '../../../utils/api/handleError';
import usePinturaEditor from '../../../hooks/usePinturaEditor';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {ReactComponent as TwitterLogo} from '../../../admin-x-ds/assets/images/twitter-logo.svg';
import {ReactComponent as TwitterLogo} from '../../../admin-x-ds/assets/images/x-logo.svg';
import {getImageUrl, useUploadImage} from '../../../api/images';
import {getSettingValues} from '../../../api/settings';
import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary';
@ -105,14 +105,14 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
clearBg={true}
inputRef={focusRef}
placeholder={siteTitle}
title="Twitter title"
title="X title"
value={twitterTitle}
onChange={handleTitleChange}
/>
<TextField
clearBg={true}
placeholder={siteDescription}
title="Twitter description"
title="X description"
value={twitterDescription}
onChange={handleDescriptionChange}
/>
@ -124,13 +124,13 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<SettingGroup
description='Customize structured data of your site'
description='Customize structured data of your site for X (formerly Twitter)'
isEditing={isEditing}
keywords={keywords}
navid='twitter'
saveState={saveState}
testId='twitter'
title='Twitter card'
title='X card'
onCancel={handleCancel}
onEditingChange={handleEditingChange}
onSave={handleSave}

View file

@ -557,15 +557,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
okLabel = 'Saved';
}
const coverButtonContainerClassName = clsx(
showMenu ? (
userData.cover_image ? 'relative ml-10 mr-[106px] flex translate-y-[-80px] gap-3 md:ml-0 md:justify-end' : 'relative -mb-8 ml-10 mr-[106px] flex translate-y-[358px] md:ml-0 md:translate-y-[268px] md:justify-end'
) : (
userData.cover_image ? 'relative ml-10 flex max-w-4xl translate-y-[-80px] gap-3 md:mx-auto md:justify-end' : 'relative -mb-8 ml-10 flex max-w-4xl translate-y-[358px] md:mx-auto md:translate-y-[268px] md:justify-end'
)
);
const coverEditButtonBaseClasses = 'bg-[rgba(0,0,0,0.75)] rounded text-sm text-white flex items-center justify-center px-3 h-8 opacity-80 hover:opacity-100 transition-all cursor-pointer font-medium';
const coverEditButtonBaseClasses = 'bg-[rgba(0,0,0,0.75)] rounded text-sm text-white flex items-center justify-center px-3 h-8 opacity-80 hover:opacity-100 transition-all cursor-pointer font-medium nowrap';
const fileUploadButtonClasses = clsx(
coverEditButtonBaseClasses
@ -637,75 +629,81 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
}}
>
<div>
<div className={`relative -mx-10 -mt-10 ${canAccessSettings(currentUser) && 'rounded-t'} bg-gradient-to-tr from-grey-900 to-black`}>
<ImageUpload
buttonContainerClassName={coverButtonContainerClassName}
deleteButtonClassName={deleteButtonClasses}
deleteButtonContent='Delete cover image'
editButtonClassName={editButtonClasses}
fileUploadClassName={fileUploadButtonClasses}
height={userData.cover_image ? '100%' : '32px'}
id='cover-image'
imageClassName='w-full h-full object-cover'
imageContainerClassName={`absolute inset-0 bg-cover group bg-center ${canAccessSettings(currentUser) && 'rounded-t'} overflow-hidden`}
imageURL={userData.cover_image || ''}
pintura={
{
isEnabled: editor.isEnabled,
openEditor: async () => editor.openEditor({
image: userData.cover_image || '',
handleSave: async (file:File) => {
handleImageUpload('cover_image', file);
}
})
}
}
unstyled={true}
onDelete={() => {
handleImageDelete('cover_image');
}}
onUpload={(file: File) => {
handleImageUpload('cover_image', file);
}}
>Upload cover image</ImageUpload>
{showMenu && <div className="absolute bottom-12 right-12 z-10">
<Menu items={menuItems} position='right' trigger={<UserMenuTrigger />}></Menu>
</div>}
<div className={`${!canAccessSettings(currentUser) ? 'mx-10 pl-0 md:max-w-[50%] min-[920px]:ml-[calc((100vw-920px)/2)] min-[920px]:max-w-[460px]' : 'max-w-[50%] pl-12'} relative flex flex-col items-start gap-4 pb-60 pt-10 md:flex-row md:items-center md:pb-7 md:pt-60`}>
<ImageUpload
deleteButtonClassName='md:invisible absolute pr-3 -right-2 -top-2 flex h-8 w-16 cursor-pointer items-center justify-end rounded-full bg-[rgba(0,0,0,0.75)] text-white group-hover:!visible'
deleteButtonContent={<Icon colorClass='text-white' name='trash' size='sm' />}
editButtonClassName='md:invisible absolute right-[22px] -top-2 flex h-8 w-8 cursor-pointer items-center justify-center text-white group-hover:!visible z-20'
fileUploadClassName='rounded-full bg-black flex items-center justify-center opacity-80 transition hover:opacity-100 -ml-2 cursor-pointer h-[80px] w-[80px]'
id='avatar'
imageClassName='w-full h-full object-cover rounded-full shrink-0'
imageContainerClassName='relative group bg-cover bg-center -ml-2 h-[80px] w-[80px] shrink-0'
imageURL={userData.profile_image}
pintura={
{
isEnabled: editor.isEnabled,
openEditor: async () => editor.openEditor({
image: userData.profile_image || '',
handleSave: async (file:File) => {
handleImageUpload('profile_image', file);
<div className={`relative ${canAccessSettings(currentUser) ? '-mx-8 -mt-8 rounded-t' : '-mx-10 -mt-10'} bg-gradient-to-tr from-grey-900 to-black`}>
<div className='flex min-h-[40vmin] flex-wrap items-end justify-between bg-cover bg-center' style={{
backgroundImage: `url(${userData.cover_image})`
}}>
<div className='flex w-full max-w-[620px] flex-col gap-5 p-8 md:max-w-[auto] md:flex-row md:items-center'>
<div>
<ImageUpload
deleteButtonClassName='md:invisible absolute pr-3 -right-2 -top-2 flex h-8 w-16 cursor-pointer items-center justify-end rounded-full bg-[rgba(0,0,0,0.75)] text-white group-hover:!visible'
deleteButtonContent={<Icon colorClass='text-white' name='trash' size='sm' />}
editButtonClassName='md:invisible absolute right-[22px] -top-2 flex h-8 w-8 cursor-pointer items-center justify-center text-white group-hover:!visible z-20'
fileUploadClassName='rounded-full bg-black flex items-center justify-center opacity-80 transition hover:opacity-100 -ml-2 cursor-pointer h-[80px] w-[80px]'
id='avatar'
imageClassName='w-full h-full object-cover rounded-full shrink-0'
imageContainerClassName='relative group bg-cover bg-center -ml-2 h-[80px] w-[80px] shrink-0'
imageURL={userData.profile_image}
pintura={
{
isEnabled: editor.isEnabled,
openEditor: async () => editor.openEditor({
image: userData.profile_image || '',
handleSave: async (file:File) => {
handleImageUpload('profile_image', file);
}
})
}
})
}
unstyled={true}
width='80px'
onDelete={() => {
handleImageDelete('profile_image');
}}
onUpload={(file: File) => {
handleImageUpload('profile_image', file);
}}
>
<Icon colorClass='text-white' name='user-add' size='lg' />
</ImageUpload>
</div>
<div>
<Heading styles='break-words md:break-normal text-white'>{user.name}{suspendedText}</Heading>
<span className='text-md font-semibold capitalize text-white'>{user.roles[0].name.toLowerCase()}</span>
</div>
</div>
<div className='flex flex-nowrap items-end gap-4 p-8'>
<ImageUpload
buttonContainerClassName='flex items-end gap-4 justify-end flex-nowrap'
deleteButtonClassName={deleteButtonClasses}
deleteButtonContent='Delete cover image'
editButtonClassName={editButtonClasses}
fileUploadClassName={fileUploadButtonClasses}
id='cover-image'
imageClassName='hidden'
imageURL={userData.cover_image || ''}
pintura={
{
isEnabled: editor.isEnabled,
openEditor: async () => editor.openEditor({
image: userData.cover_image || '',
handleSave: async (file:File) => {
handleImageUpload('cover_image', file);
}
})
}
}
}
unstyled={true}
width='80px'
onDelete={() => {
handleImageDelete('profile_image');
}}
onUpload={(file: File) => {
handleImageUpload('profile_image', file);
}}
>
<Icon colorClass='text-white' name='user-add' size='lg' />
</ImageUpload>
<div>
<Heading styles='text-white'>{user.name}{suspendedText}</Heading>
<span className='text-md font-semibold capitalize text-white'>{user.roles[0].name.toLowerCase()}</span>
unstyled
onDelete={() => {
handleImageDelete('cover_image');
}}
onUpload={(file: File) => {
handleImageUpload('cover_image', file);
}}
>Upload cover image</ImageUpload>
{showMenu && <div className="z-10">
<Menu items={menuItems} position='right' trigger={<UserMenuTrigger />}></Menu>
</div>}
</div>
</div>
</div>

View file

@ -21,11 +21,11 @@ test.describe('Social account settings', async () => {
await section.getByRole('button', {name: 'Edit'}).click();
await section.getByLabel(`URL of your publication's Facebook Page`).fill('https://www.facebook.com/fb');
await section.getByLabel('URL of your Twitter profile').fill('https://twitter.com/tw');
await section.getByLabel('URL of your X (formerly Twitter) profile').fill('https://twitter.com/tw');
await section.getByRole('button', {name: 'Save'}).click();
await expect(section.getByLabel('URL of your Twitter profile')).toHaveCount(0);
await expect(section.getByLabel('URL of your X (formerly Twitter) profile')).toHaveCount(0);
await expect(section.getByText('https://www.facebook.com/fb')).toHaveCount(1);
await expect(section.getByText('https://twitter.com/tw')).toHaveCount(1);

View file

@ -24,8 +24,8 @@ test.describe('Twitter settings', async () => {
await expect(section.getByRole('img')).toBeVisible();
await section.getByLabel('Twitter title').fill('Twititle');
await section.getByLabel('Twitter description').fill('Twitscription');
await section.getByLabel('X title').fill('Twititle');
await section.getByLabel('X description').fill('Twitscription');
await section.getByRole('button', {name: 'Save'}).click();