{
+ if (type === 'file') {
+ setSelectedFile(id);
+ }
+ }}
+ />,
+ );
+ }
+
+ return children;
+ }, [folder, selectedFile]);
+
+ return (
+ : )}
+ onClose={onClickClose}
+ confirmationAction={handleConfirm}
+ confirmationText={type === 'folder' ? : }
+ confirmationDisabled={!selectedFile && type !== 'folder'}
+ >
+
+ setCurrentFolder(folderId)} />
+
+
+ {files}
+
+
+ );
+};
+
+export { SelectDriveFileModal as default, type SelectDriveFileModalProps };
diff --git a/packages/pl-fe/src/pages/drive/drive.tsx b/packages/pl-fe/src/pages/drive/drive.tsx
index c2ff9e7f2..5c77fab7d 100644
--- a/packages/pl-fe/src/pages/drive/drive.tsx
+++ b/packages/pl-fe/src/pages/drive/drive.tsx
@@ -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 = ({ folderId, depth = 0, onClick }) => {
+ const { data } = useDriveFolderQuery(folderId);
+
+ if (!folderId) {
+ const label = depth === 0 && ;
+
+ if (onClick || depth === 0) {
+ return (
+
+ );
+ } else {
+ return (
+
+
+ {label}
+
+ );
+ }
+ }
+
+ if (!data) return null;
+
+ const spacer = (
+
+
+
+ );
+
+ const button = onClick ? (
+
+ ) : (
+
+ {data.name}
+
+ );
+
+ if (depth === 2 && data?.parent_id) {
+ return (
+ <>
+
+ {spacer}
+
+
+
+ {spacer}
+ {button}
+ >
+ );
+ }
+
+ return (
+ <>
+
+ {spacer}
+ {button}
+ >
+ );
+};
+
interface IFile {
file: DriveFile;
}
@@ -69,6 +155,7 @@ const File: React.FC = ({ 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 = ({ 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: ,
+ });
+ };
+
const handleDelete = () => {
openModal('CONFIRM', {
heading: ,
@@ -202,10 +303,11 @@ const File: React.FC = ({ 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 = ({ 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 = ({ 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: ,
+ });
+ };
+
return [
{
text: intl.formatMessage(messages.folderView),
@@ -304,6 +421,11 @@ const Folder: React.FC = ({ 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 = ({ params }) => {
backHref={data?.id === null ? '/drive' : data?.parent_id ? `/drive/${data.parent_id}` : undefined}
action={}
>
+
+
+
{isEmpty ? (
}
@@ -411,4 +536,4 @@ const DrivePage: React.FC = ({ params }) => {
);
};
-export { DrivePage as default };
+export { DrivePage as default, Breadcrumbs };
diff --git a/packages/pl-fe/src/queries/drive/use-drive-file.ts b/packages/pl-fe/src/queries/drive/use-drive-file.ts
index 8c2aac6bd..17e48f257 100644
--- a/packages/pl-fe/src/queries/drive/use-drive-file.ts
+++ b/packages/pl-fe/src/queries/drive/use-drive-file.ts
@@ -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);
diff --git a/packages/pl-fe/src/queries/drive/use-drive-folder.ts b/packages/pl-fe/src/queries/drive/use-drive-folder.ts
index 7df33e8a9..716d80e45 100644
--- a/packages/pl-fe/src/queries/drive/use-drive-folder.ts
+++ b/packages/pl-fe/src/queries/drive/use-drive-folder.ts
@@ -87,7 +87,7 @@ const useMoveDriveFolderMutation = (folderId: string) => {
return useMutation({
mutationKey: ['drive', 'folders'],
- mutationFn: (targetFolderId: string) => {
+ mutationFn: (targetFolderId?: string) => {
const oldFolder = queryClient.getQueryData(['drive', 'folders', folderId]);
if (oldFolder) {
previousParentId = oldFolder.parent_id;
diff --git a/packages/pl-fe/src/stores/modals.ts b/packages/pl-fe/src/stores/modals.ts
index 59c782d74..b9fd2834a 100644
--- a/packages/pl-fe/src/stores/modals.ts
+++ b/packages/pl-fe/src/stores/modals.ts
@@ -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];
diff --git a/packages/pl-fe/src/styles/new/drive.scss b/packages/pl-fe/src/styles/new/drive.scss
index 90f5ed808..ba45ef453 100644
--- a/packages/pl-fe/src/styles/new/drive.scss
+++ b/packages/pl-fe/src/styles/new/drive.scss
@@ -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;
+ }
+ }
}
\ No newline at end of file