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:
parent
f70dcd6861
commit
b490057429
12 changed files with 92 additions and 93 deletions
|
@ -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 |
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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}`});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue