0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Updated AdminX file upload to support drag and drop (#18691)

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

---

### <samp>🤖 Generated by Copilot at f85ceff</samp>

This pull request adds drag and drop functionality and custom styling
options to the `FileUpload` and `ImageUpload` components, and uses them
to improve the user experience of uploading themes and user images in
the admin settings. It also fixes a minor issue with the background
color contrast in the `IntegrationHeader` component in dark mode, and
updates the corresponding tests.
This commit is contained in:
Jono M 2023-10-19 11:42:00 +01:00 committed by GitHub
parent b5631241c5
commit 2debf686e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 69 additions and 19 deletions

View file

@ -1,4 +1,5 @@
import React, {CSSProperties, ChangeEvent, useState} from 'react';
import clsx from 'clsx';
export interface FileUploadProps {
id: string;
@ -10,14 +11,16 @@ export interface FileUploadProps {
*/
children?: string | React.ReactNode;
className?: string;
dragIndicatorClassName?: string;
onUpload: (file: File) => void;
style?: CSSProperties;
unstyled?: boolean;
inputRef?: React.RefObject<HTMLInputElement>;
}
const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, unstyled = false, inputRef, ...props}) => {
const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, unstyled = false, inputRef, className, dragIndicatorClassName, ...props}) => {
const [fileKey, setFileKey] = useState<number>(Date.now());
const [isDragging, setIsDragging] = useState(false);
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0];
@ -27,8 +30,28 @@ const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, u
setFileKey(Date.now());
};
const handleDrop = (event: React.DragEvent<HTMLLabelElement>) => {
handleStopDragging(event);
const selectedFile = event.dataTransfer.files?.[0];
if (selectedFile) {
onUpload?.(selectedFile);
}
setFileKey(Date.now());
};
const handleDragging = (event: React.DragEvent<HTMLLabelElement>) => {
event.preventDefault();
setIsDragging(true);
};
const handleStopDragging = (event: React.DragEvent<HTMLLabelElement>) => {
event.preventDefault();
setIsDragging(false);
};
return (
<label htmlFor={id} style={style} {...props}>
<label className={clsx('relative', className)} htmlFor={id} style={style} onDragEnter={handleDragging} onDragLeave={handleStopDragging} onDragOver={handleDragging} onDrop={handleDrop} {...props}>
{isDragging && <div className={clsx('absolute inset-1 rounded border-2 border-dashed border-grey-400/25', dragIndicatorClassName)} />}
<input key={fileKey} ref={inputRef || null} id={id} type="file" hidden onChange={handleFileChange} />
{(typeof children === 'string') ?
<div className={!unstyled ? `inline-flex h-[34px] cursor-pointer items-center justify-center rounded px-4 text-sm font-semibold hover:bg-grey-100 dark:text-white dark:hover:bg-grey-900` : ''}>

View file

@ -1,4 +1,4 @@
import FileUpload from './FileUpload';
import FileUpload, {FileUploadProps} from './FileUpload';
import Icon from '../Icon';
import React, {MouseEventHandler} from 'react';
import clsx from 'clsx';
@ -16,6 +16,7 @@ interface ImageUploadProps {
imageClassName?: string;
imageBWCheckedBg?: boolean;
fileUploadClassName?: string;
fileUploadProps?: Partial<FileUploadProps>;
deleteButtonClassName?: string;
deleteButtonContent?: React.ReactNode;
deleteButtonUnstyled?: boolean;
@ -57,6 +58,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
imageContainerClassName,
imageClassName,
fileUploadClassName,
fileUploadProps,
deleteButtonClassName,
deleteButtonContent,
deleteButtonUnstyled = false,
@ -181,7 +183,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
width: (unstyled ? '' : width),
height: (unstyled ? '' : height)
}
} unstyled={unstyled} onUpload={onUpload}>
} unstyled={unstyled} onUpload={onUpload} {...fileUploadProps}>
<>
<span className='text-center'>{children}</span>
</>

View file

@ -14,7 +14,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
extra
}) => {
return (
<div className='-mx-8 -mt-8 flex flex-col gap-4 bg-grey-75 p-8 md:flex-row'>
<div className='-mx-8 -mt-8 flex flex-col gap-4 bg-grey-75 p-8 dark:bg-grey-950 md:flex-row'>
<div className='h-14 w-14'>{icon}</div>
<div className='mt-1.5 flex min-w-0 flex-1 flex-col'>
<h3>{title}</h3>

View file

@ -417,6 +417,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
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]'
fileUploadProps={{dragIndicatorClassName: 'rounded-full'}}
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'

View file

@ -37,6 +37,24 @@ interface ThemeModalContentProps {
themes: Theme[];
}
const UploadModalContent: React.FC<{onUpload: (file: File) => void}> = ({onUpload}) => {
const modal = useModal();
return <div className="-mb-6">
<FileUpload
id="theme-upload"
onUpload={(file) => {
modal.remove();
onUpload(file);
}}
>
<div className="cursor-pointer bg-grey-75 p-10 text-center dark:bg-grey-950">
Click to select or drag & drop zip file
</div>
</FileUpload>
</div>;
};
const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
currentTab,
setCurrentTab,
@ -205,6 +223,22 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
onBack={onClose}
/>;
const handleUpload = () => {
if (uploadConfig?.enabled) {
NiceModal.show(ConfirmationModal, {
title: 'Upload theme',
prompt: <UploadModalContent onUpload={onThemeUpload} />,
okLabel: '',
formSheet: false
});
} else {
NiceModal.show(LimitModal, {
title: 'Upgrade to enable custom themes',
prompt: uploadConfig?.error || <>Your current plan only supports official themes. You can install them from the <a href="https://ghost.org/marketplace/">Ghost theme marketplace</a>.</>
});
}
};
const right =
<div className='flex items-center gap-14'>
<div className='hidden md:!visible md:!block'>
@ -220,19 +254,7 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
}} />
</div>
<div className='flex items-center gap-3'>
{uploadConfig && (
uploadConfig.enabled ?
<FileUpload id='theme-upload' inputRef={fileInputRef} onUpload={onThemeUpload}>
<Button color='black' label='Upload theme' loading={isUploading} tag='div' />
</FileUpload> :
// for when user's plan does not support custom themes
<Button color='black' label='Upload theme' onClick={() => {
NiceModal.show(LimitModal, {
title: 'Upgrade to enable custom themes',
prompt: uploadConfig?.error || <>Your current plan only supports official themes. You can install them from the <a href="https://ghost.org/marketplace/">Ghost theme marketplace</a>.</>
});
}} />
)}
<Button color='black' label='Upload theme' loading={isUploading} onClick={handleUpload} />
</div>
</div>;

View file

@ -151,9 +151,11 @@ test.describe('Theme settings', async () => {
const modal = page.getByTestId('theme-modal');
await modal.getByRole('button', {name: 'Upload theme'}).click();
const fileChooserPromise = page.waitForEvent('filechooser');
await modal.locator('label[for=theme-upload]').click();
await page.getByTestId('confirmation-modal').locator('label[for=theme-upload]').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(`${__dirname}/../../utils/responses/theme.zip`);