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:
parent
b5631241c5
commit
2debf686e6
6 changed files with 69 additions and 19 deletions
|
@ -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` : ''}>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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`);
|
||||
|
|
Loading…
Reference in a new issue