pl-fe: more drive works, allow moving folders and files
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -5948,12 +5948,12 @@ class PlApiClient {
|
||||
return response;
|
||||
},
|
||||
|
||||
moveFolder: async (id: string, targetFolderId: string) => {
|
||||
moveFolder: async (id: string, targetFolderId?: string) => {
|
||||
await this.#getIceshrimpAccessToken();
|
||||
|
||||
const response = await this.request(`/api/iceshrimp/drive/folder/${id}/move`, {
|
||||
method: 'POST',
|
||||
body: { folderId: targetFolderId },
|
||||
body: { folderId: targetFolderId || null },
|
||||
});
|
||||
|
||||
return v.parse(driveFolderSchema, response.json);
|
||||
@ -6001,12 +6001,12 @@ class PlApiClient {
|
||||
return response;
|
||||
},
|
||||
|
||||
moveFile: async (id: string, targetFolderId: string) => {
|
||||
moveFile: async (id: string, targetFolderId?: string) => {
|
||||
await this.#getIceshrimpAccessToken();
|
||||
|
||||
const response = await this.request(`/api/iceshrimp/drive/${id}/move`, {
|
||||
method: 'POST',
|
||||
body: { folderId: targetFolderId },
|
||||
body: { folderId: targetFolderId || null },
|
||||
});
|
||||
|
||||
return v.parse(driveFileSchema, response.json);
|
||||
|
||||
@ -266,6 +266,15 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.drive && (
|
||||
<DropdownNavigationLink
|
||||
to='/drive'
|
||||
icon={require('@phosphor-icons/core/regular/cloud.svg')}
|
||||
text={<FormattedMessage id='column.drive' defaultMessage='Drive' />}
|
||||
onClick={closeSidebar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.events && (
|
||||
<DropdownNavigationLink
|
||||
to='/events'
|
||||
|
||||
@ -291,6 +291,13 @@ const SidebarNavigation: React.FC<ISidebarNavigation> = React.memo(({ shrink })
|
||||
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
|
||||
/>
|
||||
|
||||
{features.drive && <SidebarNavigationLink
|
||||
to='/drive'
|
||||
icon={require('@phosphor-icons/core/regular/cloud.svg')}
|
||||
activeIcon={require('@phosphor-icons/core/fill/cloud-fill.svg')}
|
||||
text={<FormattedMessage id='column.drive' defaultMessage='Drive' />}
|
||||
/>}
|
||||
|
||||
<SidebarNavigationLink
|
||||
to='/settings'
|
||||
icon={require('@phosphor-icons/core/regular/sliders-horizontal.svg')}
|
||||
|
||||
@ -45,6 +45,7 @@ const MODAL_COMPONENTS = {
|
||||
REPLY_MENTIONS: lazy(() => import('pl-fe/modals/reply-mentions-modal')),
|
||||
REPORT: lazy(() => import('pl-fe/modals/report-modal')),
|
||||
SELECT_BOOKMARK_FOLDER: lazy(() => import('pl-fe/modals/select-bookmark-folder-modal')),
|
||||
SELECT_DRIVE_FILE: lazy(() => import('pl-fe/modals/select-drive-file-modal')),
|
||||
TEXT_FIELD: lazy(() => import('pl-fe/modals/text-field-modal')),
|
||||
UNAUTHORIZED: lazy(() => import('pl-fe/modals/unauthorized-modal')),
|
||||
};
|
||||
|
||||
@ -709,6 +709,7 @@
|
||||
"directory.recently_active": "Recently active",
|
||||
"draft_status.cancel": "Delete",
|
||||
"draft_status.edit": "Edit",
|
||||
"drive.breadcrumbs.home": "Home",
|
||||
"drive.empty": "There are no files or folders in this folder.",
|
||||
"drive.file.delete": "Delete file",
|
||||
"drive.file.delete.confirm": "Delete",
|
||||
@ -721,6 +722,9 @@
|
||||
"drive.file.mark_sensitive.error": "Failed to mark file as sensitive.",
|
||||
"drive.file.mark_sensitive.success": "File marked as sensitive.",
|
||||
"drive.file.move": "Move file",
|
||||
"drive.file.move.error": "Failed to move file.",
|
||||
"drive.file.move.heading": "Select move destination",
|
||||
"drive.file.move.success": "File moved successfully.",
|
||||
"drive.file.rename": "Rename file",
|
||||
"drive.file.rename.confirm": "Rename",
|
||||
"drive.file.rename.error": "Failed to rename file.",
|
||||
@ -746,6 +750,9 @@
|
||||
"drive.folder.delete.success": "Folder deleted successfully.",
|
||||
"drive.folder.delete.text": "Are you sure you want to delete this folder? This action cannot be undone.",
|
||||
"drive.folder.dropdown": "Folder menu",
|
||||
"drive.folder.move": "Move folder",
|
||||
"drive.folder.move.error": "Failed to move folder.",
|
||||
"drive.folder.move.success": "Folder moved successfully.",
|
||||
"drive.folder.new": "New folder",
|
||||
"drive.folder.new.error": "Failed to create folder.",
|
||||
"drive.folder.new.placeholder": "Folder name",
|
||||
@ -756,6 +763,10 @@
|
||||
"drive.folder.rename.placeholder": "New folder name",
|
||||
"drive.folder.rename.success": "Folder renamed successfully.",
|
||||
"drive.folder.view": "View folder",
|
||||
"drive.select_file.confirm": "Select file",
|
||||
"drive.select_file.heading": "Select file",
|
||||
"drive.select_folder.confirm": "Select folder",
|
||||
"drive.select_folder.heading": "Select folder",
|
||||
"edit_bookmark_folder_modal.confirm": "Save",
|
||||
"edit_bookmark_folder_modal.header_title": "Edit folder",
|
||||
"edit_email.header": "Change email",
|
||||
|
||||
189
packages/pl-fe/src/modals/select-drive-file-modal.tsx
Normal file
189
packages/pl-fe/src/modals/select-drive-file-modal.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import defaultIcon from '@phosphor-icons/core/regular/paperclip.svg';
|
||||
import clsx from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Modal from 'pl-fe/components/ui/modal';
|
||||
import { MIMETYPE_ICONS } from 'pl-fe/components/upload';
|
||||
import { Breadcrumbs } from 'pl-fe/pages/drive/drive';
|
||||
import { useDriveFolderQuery } from 'pl-fe/queries/drive/use-drive-folder';
|
||||
|
||||
import type { DriveFile, DriveFolder } from 'pl-api';
|
||||
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
|
||||
|
||||
type SelectDriveFileModalProps = {
|
||||
disabled?: Array<string | null>;
|
||||
title?: React.ReactNode;
|
||||
} & ({
|
||||
type: 'file';
|
||||
onSelect: (file: DriveFile) => void;
|
||||
} | {
|
||||
type: 'folder';
|
||||
onSelect: (folder: DriveFolder) => void;
|
||||
});
|
||||
|
||||
interface IFolder {
|
||||
folder: DriveFolder;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
onSelect?: (folder: DriveFolder) => void;
|
||||
onDoubleClick?: (folder: DriveFolder) => void;
|
||||
}
|
||||
|
||||
const Folder: React.FC<IFolder> = ({ folder, active, disabled, onSelect, onDoubleClick }) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx('⁂-drive-file ⁂-drive-folder', { '⁂-drive-file--active': active, '⁂-drive-file--disabled': disabled })}
|
||||
tabIndex={0}
|
||||
onDoubleClick={disabled ? undefined : () => onDoubleClick?.(folder)}
|
||||
onClick={disabled ? undefined : () => onSelect?.(folder)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
className='⁂-drive-file__icon'
|
||||
src={require('@phosphor-icons/core/regular/folder.svg')}
|
||||
/>
|
||||
|
||||
<span className='⁂-drive-file__label'>
|
||||
{folder.name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IFile {
|
||||
file: DriveFile;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
onSelect?: (file: DriveFile) => void;
|
||||
}
|
||||
|
||||
const File: React.FC<IFile> = ({ file, active, disabled, onSelect }) => {
|
||||
const isMedia = file.content_type.match(/image|video|audio/);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx('⁂-drive-file', { '⁂-drive-file--active': active, '⁂-drive-file--disabled': disabled })}
|
||||
tabIndex={0}
|
||||
onClick={disabled ? undefined : () => onSelect?.(file)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{file.thumbnail_url && isMedia ? (
|
||||
<img
|
||||
src={file.thumbnail_url}
|
||||
alt={file.description || undefined}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
className='⁂-drive-file__icon'
|
||||
src={MIMETYPE_ICONS[file.content_type || ''] || defaultIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className='⁂-drive-file__label'>
|
||||
{file.filename}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectDriveFileModal: React.FC<SelectDriveFileModalProps & BaseModalProps> = ({ onClose, onSelect, type, disabled, title }) => {
|
||||
const onClickClose = () => {
|
||||
onClose('SELECT_DRIVE_FILE');
|
||||
};
|
||||
|
||||
const [currentFolder, setCurrentFolder] = React.useState<string>();
|
||||
const [selectedFile, setSelectedFile] = React.useState<string>();
|
||||
|
||||
const { data: folder } = useDriveFolderQuery(currentFolder);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!folder) return;
|
||||
|
||||
if (type === 'file') {
|
||||
const file = folder.files.find(({ id }) => id === selectedFile);
|
||||
if (file) {
|
||||
onSelect(file);
|
||||
}
|
||||
} else {
|
||||
const selectedFolder = folder.folders.find(({ id }) => id === selectedFile);
|
||||
if (selectedFolder) {
|
||||
onSelect(selectedFolder);
|
||||
} else if (!disabled?.includes(folder?.id || null)) {
|
||||
onSelect(folder);
|
||||
}
|
||||
}
|
||||
|
||||
onClose('SELECT_DRIVE_FILE');
|
||||
};
|
||||
|
||||
const files = useMemo(() => {
|
||||
const children: React.ReactNode[] = [];
|
||||
|
||||
if (!folder) return children;
|
||||
|
||||
for (const subfolder of folder.folders) {
|
||||
children.push(
|
||||
<Folder
|
||||
key={subfolder.id}
|
||||
folder={subfolder}
|
||||
active={selectedFile === subfolder.id}
|
||||
disabled={disabled?.includes(subfolder.id)}
|
||||
onSelect={({ id }) => {
|
||||
if (type === 'folder') {
|
||||
setSelectedFile(id || undefined);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={({ id }) => {
|
||||
setCurrentFolder(id || undefined);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
for (const file of folder.files) {
|
||||
children.push(
|
||||
<File
|
||||
key={file.id}
|
||||
file={file}
|
||||
active={selectedFile === file.id}
|
||||
disabled={type === 'folder' || disabled?.includes(file.id)}
|
||||
onSelect={({ id }) => {
|
||||
if (type === 'file') {
|
||||
setSelectedFile(id);
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}, [folder, selectedFile]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title ?? (type === 'folder' ? <FormattedMessage id='drive.select_folder.heading' defaultMessage='Select folder' /> : <FormattedMessage id='drive.select_file.heading' defaultMessage='Select file' />)}
|
||||
onClose={onClickClose}
|
||||
confirmationAction={handleConfirm}
|
||||
confirmationText={type === 'folder' ? <FormattedMessage id='drive.select_folder.confirm' defaultMessage='Select folder' /> : <FormattedMessage id='drive.select_file.confirm' defaultMessage='Select file' />}
|
||||
confirmationDisabled={!selectedFile && type !== 'folder'}
|
||||
>
|
||||
<div className='⁂-drive-breadcrumbs'>
|
||||
<Breadcrumbs folderId={currentFolder} onClick={(folderId) => setCurrentFolder(folderId)} />
|
||||
</div>
|
||||
<ScrollableList
|
||||
listClassName='⁂-drive-file-list divide-y divide-solid divide-gray-200 black:divide-gray-800 dark:divide-primary-800'
|
||||
style={{ minHeight: 'calc(80vh - 192px)' }}
|
||||
isLoading={!folder}
|
||||
showLoading={!folder}
|
||||
useWindowScroll={false}
|
||||
>
|
||||
{files}
|
||||
</ScrollableList>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { SelectDriveFileModal as default, type SelectDriveFileModalProps };
|
||||
@ -1,7 +1,8 @@
|
||||
import defaultIcon from '@phosphor-icons/core/regular/paperclip.svg';
|
||||
import { clsx } from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu';
|
||||
import { EmptyMessage } from 'pl-fe/components/empty-message';
|
||||
@ -10,8 +11,8 @@ 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 { useCreateDriveFileMutation, useDeleteDriveFileMutation, useUpdateDriveFileMutation } from 'pl-fe/queries/drive/use-drive-file';
|
||||
import { useCreateDriveFolderMutation, useDeleteDriveFolderMutation, useDriveFolderQuery, useUpdateDriveFolderMutation } from 'pl-fe/queries/drive/use-drive-folder';
|
||||
import { useCreateDriveFileMutation, useDeleteDriveFileMutation, useMoveDriveFileMutation, useUpdateDriveFileMutation } from 'pl-fe/queries/drive/use-drive-file';
|
||||
import { useCreateDriveFolderMutation, useDeleteDriveFolderMutation, useDriveFolderQuery, useMoveDriveFolderMutation, 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';
|
||||
@ -26,6 +27,9 @@ const messages = defineMessages({
|
||||
folderRenamePlaceholder: { id: 'drive.folder.rename.placeholder', defaultMessage: 'New folder name' },
|
||||
folderRenameSuccess: { id: 'drive.folder.rename.success', defaultMessage: 'Folder renamed successfully.' },
|
||||
folderRenameError: { id: 'drive.folder.rename.error', defaultMessage: 'Failed to rename folder.' },
|
||||
folderMove: { id: 'drive.folder.move', defaultMessage: 'Move folder' },
|
||||
folderMoveSuccess: { id: 'drive.folder.move.success', defaultMessage: 'Folder moved successfully.' },
|
||||
folderMoveError: { id: 'drive.folder.move.error', defaultMessage: 'Failed to move folder.' },
|
||||
folderDelete: { id: 'drive.folder.delete', defaultMessage: 'Delete folder' },
|
||||
folderDeleteSuccess: { id: 'drive.folder.delete.success', defaultMessage: 'Folder deleted successfully.' },
|
||||
folderDeleteError: { id: 'drive.folder.delete.error', defaultMessage: 'Failed to delete folder.' },
|
||||
@ -47,6 +51,8 @@ const messages = defineMessages({
|
||||
unmarkSensitiveSuccess: { id: 'drive.file.unmark_sensitive.success', defaultMessage: 'File unmarked as sensitive.' },
|
||||
unmarkSensitiveError: { id: 'drive.file.unmark_sensitive.error', defaultMessage: 'Failed to unmark file as sensitive.' },
|
||||
fileMove: { id: 'drive.file.move', defaultMessage: 'Move file' },
|
||||
fileMoveSuccess: { id: 'drive.file.move.success', defaultMessage: 'File moved successfully.' },
|
||||
fileMoveError: { id: 'drive.file.move.error', defaultMessage: 'Failed to move file.' },
|
||||
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.' },
|
||||
@ -59,6 +65,86 @@ const messages = defineMessages({
|
||||
newFolderError: { id: 'drive.folder.new.error', defaultMessage: 'Failed to create folder.' },
|
||||
});
|
||||
|
||||
interface IBreadcrumbs {
|
||||
folderId?: string;
|
||||
depth?: number;
|
||||
onClick?: (folderId?: string) => void;
|
||||
}
|
||||
|
||||
const Breadcrumbs: React.FC<IBreadcrumbs> = ({ folderId, depth = 0, onClick }) => {
|
||||
const { data } = useDriveFolderQuery(folderId);
|
||||
|
||||
if (!folderId) {
|
||||
const label = depth === 0 && <span><FormattedMessage id='drive.breadcrumbs.home' defaultMessage='Home' /></span>;
|
||||
|
||||
if (onClick || depth === 0) {
|
||||
return (
|
||||
<button
|
||||
className={clsx('⁂-drive-breadcrumbs__item ⁂-drive-breadcrumbs__home', { '⁂-drive-breadcrumbs__item--current': depth === 0 })}
|
||||
onClick={() => onClick?.()}
|
||||
disabled={depth === 0}
|
||||
>
|
||||
<Icon src={require('@phosphor-icons/core/regular/house.svg')} />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link to='/drive' className='⁂-drive-breadcrumbs__home'>
|
||||
<Icon src={require('@phosphor-icons/core/regular/house.svg')} />
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const spacer = (
|
||||
<div className='⁂-drive-breadcrumbs__spacer' aria-hidden>
|
||||
<Icon src={require('@phosphor-icons/core/regular/caret-right.svg')} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const button = onClick ? (
|
||||
<button
|
||||
className={clsx('⁂-drive-breadcrumbs__item', { '⁂-drive-breadcrumbs__item--current': depth === 0 })}
|
||||
onClick={() => onClick?.(folderId)}
|
||||
>
|
||||
{data.name}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to={`/drive/${folderId}`}
|
||||
className={clsx('⁂-drive-breadcrumbs__item', { '⁂-drive-breadcrumbs__item--current': depth === 0 })}
|
||||
>
|
||||
{data.name}
|
||||
</Link>
|
||||
);
|
||||
|
||||
if (depth === 2 && data?.parent_id) {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs depth={depth + 1} onClick={onClick} />
|
||||
{spacer}
|
||||
<div className='⁂-drive-breadcrumbs__spacer' aria-hidden>
|
||||
<Icon src={require('@phosphor-icons/core/regular/dots-three.svg')} />
|
||||
</div>
|
||||
{spacer}
|
||||
{button}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs folderId={data.parent_id || undefined} depth={depth + 1} onClick={onClick} />
|
||||
{spacer}
|
||||
{button}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IFile {
|
||||
file: DriveFile;
|
||||
}
|
||||
@ -69,6 +155,7 @@ const File: React.FC<IFile> = ({ file }) => {
|
||||
const { openModal } = useModalsActions();
|
||||
const { mutate: updateFile } = useUpdateDriveFileMutation(file.id);
|
||||
const { mutate: deleteFile } = useDeleteDriveFileMutation(file.id);
|
||||
const { mutate: moveFile } = useMoveDriveFileMutation(file.id);
|
||||
|
||||
const isMedia = file.content_type.match(/image|video|audio/);
|
||||
|
||||
@ -158,6 +245,20 @@ const File: React.FC<IFile> = ({ file }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleMove = () => {
|
||||
openModal('SELECT_DRIVE_FILE', {
|
||||
type: 'folder',
|
||||
onSelect: (targetFolder) => {
|
||||
moveFile(targetFolder.id || undefined, {
|
||||
onSuccess: () => toast.success(messages.fileMoveSuccess),
|
||||
onError: () => toast.error(messages.fileMoveError),
|
||||
});
|
||||
},
|
||||
disabled: [file.id],
|
||||
title: <FormattedMessage id='drive.file.move.heading' defaultMessage='Select move destination' />,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
openModal('CONFIRM', {
|
||||
heading: <FormattedMessage id='drive.file.delete' defaultMessage='Delete file' />,
|
||||
@ -202,10 +303,11 @@ const File: React.FC<IFile> = ({ file }) => {
|
||||
action: handleToggleSensitive,
|
||||
},
|
||||
null,
|
||||
// {
|
||||
// text: intl.formatMessage(messages.fileMove),
|
||||
// icon: require('@phosphor-icons/core/regular/folders.svg'),
|
||||
// },
|
||||
{
|
||||
text: intl.formatMessage(messages.fileMove),
|
||||
icon: require('@phosphor-icons/core/regular/folders.svg'),
|
||||
action: handleMove,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.fileDelete),
|
||||
icon: require('@phosphor-icons/core/regular/trash.svg'),
|
||||
@ -257,6 +359,7 @@ const Folder: React.FC<IFolder> = ({ folder }) => {
|
||||
const { openModal } = useModalsActions();
|
||||
const { mutate: deleteFolder } = useDeleteDriveFolderMutation(folder.id!);
|
||||
const { mutate: updateFolder } = useUpdateDriveFolderMutation(folder.id!);
|
||||
const { mutate: moveFolder } = useMoveDriveFolderMutation(folder.id!);
|
||||
|
||||
const handleEnterFolder = () => {
|
||||
history.push(`/drive/${folder.id}`);
|
||||
@ -293,6 +396,20 @@ const Folder: React.FC<IFolder> = ({ folder }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleMove = () => {
|
||||
openModal('SELECT_DRIVE_FILE', {
|
||||
type: 'folder',
|
||||
onSelect: (targetFolder) => {
|
||||
moveFolder(targetFolder.id || undefined, {
|
||||
onSuccess: () => toast.success(messages.folderMoveSuccess),
|
||||
onError: () => toast.error(messages.folderMoveError),
|
||||
});
|
||||
},
|
||||
disabled: [folder.id],
|
||||
title: <FormattedMessage id='drive.file.move.heading' defaultMessage='Select move destination' />,
|
||||
});
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
text: intl.formatMessage(messages.folderView),
|
||||
@ -304,6 +421,11 @@ const Folder: React.FC<IFolder> = ({ folder }) => {
|
||||
icon: require('@phosphor-icons/core/regular/cursor-text.svg'),
|
||||
action: handleRename,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.folderMove),
|
||||
icon: require('@phosphor-icons/core/regular/folders.svg'),
|
||||
action: handleMove,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.folderDelete),
|
||||
icon: require('@phosphor-icons/core/regular/trash.svg'),
|
||||
@ -396,6 +518,9 @@ const DrivePage: React.FC<IDrivePage> = ({ params }) => {
|
||||
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')} />}
|
||||
>
|
||||
<div className='⁂-drive-breadcrumbs'>
|
||||
<Breadcrumbs folderId={params?.folderId} />
|
||||
</div>
|
||||
{isEmpty ? (
|
||||
<EmptyMessage
|
||||
text={<FormattedMessage id='drive.empty' defaultMessage='There are no files or folders in this folder.' />}
|
||||
@ -411,4 +536,4 @@ const DrivePage: React.FC<IDrivePage> = ({ params }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { DrivePage as default };
|
||||
export { DrivePage as default, Breadcrumbs };
|
||||
|
||||
@ -63,7 +63,7 @@ const useMoveDriveFileMutation = (fileId: string) => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['drive', 'files'],
|
||||
mutationFn: (folderId: string) => client.drive.moveFile(fileId, folderId),
|
||||
mutationFn: (folderId?: string) => client.drive.moveFile(fileId, folderId),
|
||||
onSuccess: (file) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['drive', 'folders'], exact: false });
|
||||
queryClient.setQueryData(['drive', 'files', file.id], file);
|
||||
|
||||
@ -87,7 +87,7 @@ const useMoveDriveFolderMutation = (folderId: string) => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['drive', 'folders'],
|
||||
mutationFn: (targetFolderId: string) => {
|
||||
mutationFn: (targetFolderId?: string) => {
|
||||
const oldFolder = queryClient.getQueryData<DriveFolder>(['drive', 'folders', folderId]);
|
||||
if (oldFolder) {
|
||||
previousParentId = oldFolder.parent_id;
|
||||
|
||||
@ -35,6 +35,7 @@ import type { ReblogsModalProps } from 'pl-fe/modals/reblogs-modal';
|
||||
import type { ReplyMentionsModalProps } from 'pl-fe/modals/reply-mentions-modal';
|
||||
import type { ReportModalProps } from 'pl-fe/modals/report-modal';
|
||||
import type { SelectBookmarkFolderModalProps } from 'pl-fe/modals/select-bookmark-folder-modal';
|
||||
import type { SelectDriveFileModalProps } from 'pl-fe/modals/select-drive-file-modal';
|
||||
import type { TextFieldModalProps } from 'pl-fe/modals/text-field-modal';
|
||||
import type { UnauthorizedModalProps } from 'pl-fe/modals/unauthorized-modal';
|
||||
|
||||
@ -73,6 +74,7 @@ type OpenModalProps =
|
||||
| [type: 'REPLY_MENTIONS', props: ReplyMentionsModalProps]
|
||||
| [type: 'REPORT', props: ReportModalProps]
|
||||
| [type: 'SELECT_BOOKMARK_FOLDER', props: SelectBookmarkFolderModalProps]
|
||||
| [type: 'SELECT_DRIVE_FILE', props: SelectDriveFileModalProps]
|
||||
| [type: 'TEXT_FIELD', props: TextFieldModalProps]
|
||||
| [type: 'UNAUTHORIZED', props?: UnauthorizedModalProps];
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use 'mixins';
|
||||
|
||||
.⁂-drive-page {
|
||||
&__files {
|
||||
display: flex;
|
||||
@ -37,6 +39,7 @@
|
||||
width: 6rem;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
@ -68,4 +71,109 @@
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.⁂-drive-breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
gap: 0.125rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__spacer {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&--current {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgb(var(--color-gray-200));
|
||||
|
||||
&:is(.dark *) {
|
||||
background: rgb(var(--color-primary-700));
|
||||
}
|
||||
|
||||
&:is(.dark.black *) {
|
||||
background: rgb(var(--color-gray-800));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__home {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
span {
|
||||
@include mixins.text($size: sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.⁂-drive-file-list {
|
||||
.⁂-drive-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
pointer-events: none;
|
||||
margin: 0.5rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin: 0.5rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
color: rgb(var(--color-gray-800));
|
||||
|
||||
&:is(.dark *) {
|
||||
color: rgb(var(--color-gray-200));
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
// margin-top: auto;
|
||||
// overflow: hidden;
|
||||
// display: inline;
|
||||
// display: -webkit-box;
|
||||
// -webkit-box-orient: vertical;
|
||||
// -webkit-line-clamp: 3;
|
||||
// max-width: 100%;
|
||||
// text-overflow: ellipsis;
|
||||
// border-radius: 0.25rem;
|
||||
// background-color: rgb(var(--color-gray-900));
|
||||
// padding: 0.25rem 0.5rem;
|
||||
// font-size: 0.75rem;
|
||||
// line-height: 1rem;
|
||||
// font-weight: 500;
|
||||
// color: #fff;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: rgb(var(--color-primary-100));
|
||||
|
||||
&:is(.dark *) {
|
||||
background: rgb(var(--color-primary-800));
|
||||
}
|
||||
|
||||
&:is(.dark.black *) {
|
||||
background: rgb(var(--color-gray-900));
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user