pl-fe: allow uploading files to drive

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-11-24 14:19:34 +01:00
parent f71ec7aa90
commit f087e35f65
5 changed files with 89 additions and 26 deletions

View File

@ -5957,7 +5957,8 @@ class PlApiClient {
const response = await this.request('/api/iceshrimp/drive', {
method: 'POST',
body: { file, folderId },
body: { file },
params: { folderId },
contentType: '',
});

View File

@ -23,6 +23,8 @@ interface MenuItem {
to?: string;
type?: 'toggle' | 'radio';
items?: Array<Omit<MenuItem, 'items'>>;
onSelectFile?: (files: FileList) => void;
accept?: string;
}
interface IDropdownMenuItem {
@ -37,6 +39,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
const history = useHistory();
const itemRef = useRef<HTMLAnchorElement>(null);
const fileElement = useRef<HTMLInputElement>(null);
const handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = (event) => {
event.stopPropagation();
@ -49,6 +52,11 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
return;
}
if (item.onSelectFile) {
fileElement.current?.click();
return;
}
if (onClick) onClick(!(item.to && userTouching.matches));
if (item.to) {
@ -65,6 +73,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
const handleAuxClick: React.EventHandler<React.MouseEvent> = (event) => {
if (!item) return;
if (item.onSelectFile) fileElement.current?.click();
if (onClick) onClick();
if (event.button === 1 && item.middleClick) {
@ -87,6 +96,14 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
if (item.onChange) item.onChange(event.target.checked);
};
const handleSelectFileChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
console.log('handleSelectFileChange');
console.log(e.target.files, item);
if (e.target.files?.length && item?.onSelectFile) {
item.onSelectFile(e.target.files);
}
};
useEffect(() => {
const firstItem = index === 0;
@ -145,6 +162,19 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
<Icon src={require('@phosphor-icons/core/regular/caret-right.svg')} containerClassName='ml-auto' className='size-5 flex-none' />
)}
</a>
{item.onSelectFile && (
<label className='sr-only'>
<span>{item.text}</span>
<input
ref={fileElement}
type='file'
accept={item.accept}
onChange={handleSelectFileChange}
className='hidden'
/>
</label>
)}
</li>
);
};

View File

@ -734,6 +734,9 @@
"drive.file.update_description.error": "Failed to update description.",
"drive.file.update_description.placeholder": "New description",
"drive.file.update_description.success": "Description updated successfully.",
"drive.file.upload": "Upload file",
"drive.file.upload.error": "Failed to upload file.",
"drive.file.upload.success": "File uploaded successfully.",
"drive.file.view": "View file",
"drive.folder.delete": "Delete folder",
"drive.folder.delete.confirm": "Delete",

View File

@ -1,6 +1,7 @@
import defaultIcon from '@phosphor-icons/core/regular/paperclip.svg';
import React, { useMemo } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu';
import { EmptyMessage } from 'pl-fe/components/empty-message';
@ -10,10 +11,11 @@ import Icon from 'pl-fe/components/ui/icon';
import IconButton from 'pl-fe/components/ui/icon-button';
import { MIMETYPE_ICONS } from 'pl-fe/components/upload';
import ColumnLoading from 'pl-fe/features/ui/components/column-loading';
import { useDeleteDriveFileMutation, useUpdateDriveFileMutation } from 'pl-fe/queries/drive/use-drive-file';
import { useCreateDriveFileMutation, useDeleteDriveFileMutation, useUpdateDriveFileMutation } from 'pl-fe/queries/drive/use-drive-file';
import { useDeleteDriveFolderMutation, useDriveFolderQuery, useUpdateDriveFolderMutation } from 'pl-fe/queries/drive/use-drive-folder';
import { useModalsActions } from 'pl-fe/stores/modals';
import toast from 'pl-fe/toast';
import { download } from 'pl-fe/utils/download';
import type { DriveFile, DriveFolder, MediaAttachment } from 'pl-api';
@ -49,6 +51,9 @@ const messages = defineMessages({
fileDelete: { id: 'drive.file.delete', defaultMessage: 'Delete file' },
fileDeleteSuccess: { id: 'drive.file.delete.success', defaultMessage: 'File deleted successfully.' },
fileDeleteError: { id: 'drive.file.delete.error', defaultMessage: 'Failed to delete file.' },
fileUpload: { id: 'drive.file.upload', defaultMessage: 'Upload file' },
fileUploadSuccess: { id: 'drive.file.upload.success', defaultMessage: 'File uploaded successfully.' },
fileUploadError: { id: 'drive.file.upload.error', defaultMessage: 'Failed to upload file.' },
});
interface IFile {
@ -64,25 +69,30 @@ const File: React.FC<IFile> = ({ file }) => {
const isMedia = file.content_type.match(/image|video|audio/);
const handleView = () => {
if (!isMedia) {
download(file.url, file.filename);
return;
}
const mediaAttachment = {
id: file.id,
url: file.url,
preview_url: file.thumbnail_url,
remote_url: file.url,
description: file.description || '',
type: file.content_type.split('/')[0] as 'image' | 'video' | 'audio' | 'unknown',
mime_type: file.content_type,
blurhash: null,
} as MediaAttachment;
openModal('MEDIA', {
media: [mediaAttachment],
index: 0,
});
};
const items = useMemo(() => {
const handleView = () => {
const mediaAttachment = {
id: file.id,
url: file.url,
preview_url: file.thumbnail_url,
remote_url: file.url,
description: file.description || '',
type: file.content_type.split('/')[0] as 'image' | 'video' | 'audio' | 'unknown',
mime_type: file.content_type,
blurhash: null,
} as MediaAttachment;
openModal('MEDIA', {
media: [mediaAttachment],
index: 0,
});
};
const handleRename = () => {
openModal('TEXT_FIELD', {
heading: <FormattedMessage id='drive.file.rename' defaultMessage='Rename file' />,
@ -203,7 +213,7 @@ const File: React.FC<IFile> = ({ file }) => {
}, [file]);
return (
<div className='group relative flex w-32 flex-col items-center gap-2' tabIndex={0}>
<div className='group relative flex w-32 flex-col items-center gap-2' tabIndex={0} onDoubleClick={handleView}>
<div className='invisible absolute self-end group-hover:visible group-focus:visible'>
<DropdownMenu items={items} placement='right-start'>
<IconButton
@ -241,12 +251,17 @@ interface IFolder {
}
const Folder: React.FC<IFolder> = ({ folder }) => {
const history = useHistory();
const intl = useIntl();
const { openModal } = useModalsActions();
const { mutate: deleteFolder } = useDeleteDriveFolderMutation(folder.id!);
const { mutate: updateFolder } = useUpdateDriveFolderMutation(folder.id!);
const handleEnterFolder = () => {
history.push(`/drive/${folder.id}`);
};
const items: Menu = useMemo(() => {
const handleRename = () => {
openModal('TEXT_FIELD', {
@ -299,7 +314,7 @@ const Folder: React.FC<IFolder> = ({ folder }) => {
}, [folder]);
return (
<div className='group relative flex w-32 flex-col items-center gap-2' tabIndex={0}>
<div className='group relative flex w-32 flex-col items-center gap-2' tabIndex={0} onDoubleClick={handleEnterFolder}>
<div className='invisible absolute self-end group-hover:visible group-focus:visible'>
<DropdownMenu items={items} placement='right-start'>
<IconButton
@ -334,6 +349,20 @@ const DrivePage: React.FC<IDrivePage> = ({ params }) => {
const intl = useIntl();
const { data, isPending } = useDriveFolderQuery(params?.folderId);
const { mutate: uploadFile } = useCreateDriveFileMutation(params?.folderId);
const items: Menu = [
{
text: intl.formatMessage(messages.fileUpload),
icon: require('@phosphor-icons/core/regular/upload.svg'),
onSelectFile: (files: FileList) => {
uploadFile(files[0], {
onSuccess: () => toast.success(messages.fileUploadSuccess),
onError: (error) => toast.error(messages.fileUploadError),
});
},
},
];
if (isPending) {
return <ColumnLoading />;
@ -345,7 +374,7 @@ const DrivePage: React.FC<IDrivePage> = ({ params }) => {
<Column
label={data?.name || intl.formatMessage(messages.heading)}
backHref={data?.id === null ? '/drive' : data?.parent_id ? `/drive/${data.parent_id}` : undefined}
// action={<DropdownMenu items={items} src={require('@phosphor-icons/core/regular/dots-three-vertical.svg')} />}
action={<DropdownMenu items={items} src={require('@phosphor-icons/core/regular/dots-three-vertical.svg')} />}
>
{isEmpty ? (
<EmptyMessage

View File

@ -16,14 +16,14 @@ const useDriveFileQuery = (fileId: string) => {
});
};
const useCreateDriveFileMutation = () => {
const useCreateDriveFileMutation = (folderId?: string) => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['drive', 'files'],
mutationFn: ({ file, folderId }: { file: File; folderId?: string }) => client.drive.createFile(file, folderId),
onSuccess: (file, { folderId }) => {
mutationFn: (file: File) => client.drive.createFile(file, folderId),
onSuccess: (file) => {
queryClient.setQueryData(['drive', 'files', file.id], file);
queryClient.invalidateQueries({ queryKey: ['drive', 'folders', folderId], exact: true });
},