From f087e35f651af11f811ac2f72c6260f693774602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 24 Nov 2025 14:19:34 +0100 Subject: [PATCH] pl-fe: allow uploading files to drive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/lib/client.ts | 3 +- .../dropdown-menu/dropdown-menu-item.tsx | 30 ++++++++ packages/pl-fe/src/locales/en.json | 3 + packages/pl-fe/src/pages/drive/drive.tsx | 73 +++++++++++++------ .../pl-fe/src/queries/drive/use-drive-file.ts | 6 +- 5 files changed, 89 insertions(+), 26 deletions(-) diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index 0f70aff84..55bd5011d 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -5957,7 +5957,8 @@ class PlApiClient { const response = await this.request('/api/iceshrimp/drive', { method: 'POST', - body: { file, folderId }, + body: { file }, + params: { folderId }, contentType: '', }); diff --git a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx index 343776e90..3c500e9ac 100644 --- a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx +++ b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx @@ -23,6 +23,8 @@ interface MenuItem { to?: string; type?: 'toggle' | 'radio'; items?: Array>; + 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(null); + const fileElement = useRef(null); const handleClick: React.EventHandler = (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 = (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 = (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 )} + + {item.onSelectFile && ( + + )} ); }; diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 53b233185..c6136e7f1 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -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", diff --git a/packages/pl-fe/src/pages/drive/drive.tsx b/packages/pl-fe/src/pages/drive/drive.tsx index 249ec563c..d5cd89a3d 100644 --- a/packages/pl-fe/src/pages/drive/drive.tsx +++ b/packages/pl-fe/src/pages/drive/drive.tsx @@ -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 = ({ 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: , @@ -203,7 +213,7 @@ const File: React.FC = ({ file }) => { }, [file]); return ( -
+
= ({ 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 = ({ folder }) => { }, [folder]); return ( -
+
= ({ 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 ; @@ -345,7 +374,7 @@ const DrivePage: React.FC = ({ params }) => { } + action={} > {isEmpty ? ( { }); }; -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 }); },