diff --git a/packages/common-components/src/Breadcrumb/Breadcrumb.tsx b/packages/common-components/src/Breadcrumb/Breadcrumb.tsx index e6072e3aa8..19131c86d0 100644 --- a/packages/common-components/src/Breadcrumb/Breadcrumb.tsx +++ b/packages/common-components/src/Breadcrumb/Breadcrumb.tsx @@ -88,6 +88,9 @@ const useStyles = makeStyles( menuTitle: { padding: `0px ${constants.generalUnit * 1.5}px 0px ${constants.generalUnit * 0.5}px` }, + menuOptions: { + zIndex: zIndex?.layer1 + }, menuIcon: { width: 12, height: 12, @@ -158,6 +161,7 @@ const Breadcrumb = ({ animation="rotate" classNames={{ item: classes.menuItem, + options: classes.menuOptions, title: classes.menuTitle, icon: classes.menuIcon, titleText: classes.menuTitleText diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx index dea07377db..85ce8b84dd 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx @@ -382,6 +382,7 @@ const FileSystemItem = ({ canDrop: (item) => isFolder && !item.ids.includes(file.cid), drop: (item: { ids: string[]}) => { moveItems && moveItems(item.ids, getPathWithFile(currentPath, name)) + resetSelectedFiles() }, collect: (monitor) => ({ isOverMove: monitor.isOver() && !monitor.getItem<{ids: string[]}>().ids.includes(file.cid) diff --git a/packages/storage-ui/src/App.tsx b/packages/storage-ui/src/App.tsx index fabe5f80c1..53b91322ca 100644 --- a/packages/storage-ui/src/App.tsx +++ b/packages/storage-ui/src/App.tsx @@ -18,6 +18,13 @@ import { NotificationsProvider } from "./Contexts/NotificationsContext" import { PosthogProvider } from "./Contexts/PosthogContext" import { HelmetProvider } from "react-helmet-async" import ErrorModal from "./Components/Modules/ErrorModal" +import { StylesProvider, createGenerateClassName } from "@material-ui/styles" + +// making material and jss use one className generator +const generateClassName = createGenerateClassName({ + productionPrefix: "c", + disableGlobal: true +}) if ( process.env.NODE_ENV === "production" && @@ -67,49 +74,51 @@ const App = () => { return ( - - window.location.reload()} + + - - - - - window.location.reload()} + > + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx b/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx index ed628289f0..35bbefa834 100644 --- a/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx +++ b/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx @@ -3,10 +3,6 @@ import { FormikTextInput, Typography, Button, - FileImageSvg, - FilePdfSvg, - FileTextSvg, - FolderFilledSvg, DownloadSvg, DeleteSvg, EditSvg, @@ -32,6 +28,7 @@ import { BrowserView, FileOperation } from "../../../Contexts/types" import { DragTypes } from "../FilesList/DragConstants" import { nameValidator } from "../../../Utils/validationSchema" import { getPathWithFile } from "../../../Utils/pathUtils" +import { getIconForItem } from "../../../Utils/getItemIcon" const useStyles = makeStyles(({ breakpoints, constants }: CSSTheme) => { return createStyles({ @@ -142,17 +139,7 @@ const FileSystemItem = ({ resetSelectedFiles }: IFileSystemItemProps) => { const { downloadFile, currentPath, handleUploadOnDrop, moveItems } = useFileBrowser() - const { cid, name, isFolder, content_type } = file - let Icon - if (isFolder) { - Icon = FolderFilledSvg - } else if (content_type.includes("image")) { - Icon = FileImageSvg - } else if (content_type.includes("pdf")) { - Icon = FilePdfSvg - } else { - Icon = FileTextSvg - } + const { cid, name, isFolder } = file const { desktop } = useThemeSwitcher() const classes = useStyles() @@ -163,11 +150,16 @@ const FileSystemItem = ({ onSubmit: (values: {name: string}) => { const newName = values.name.trim() - editingFile && newName && handleRename && handleRename(editingFile, newName) + editingFile && newName && newName !== name && handleRename && handleRename(editingFile, newName) }, enableReinitialize: true }) + const stopEditing = useCallback(() => { + setEditingFile(undefined) + formik.resetForm() + }, [formik, setEditingFile]) + const allMenuItems: Record = { rename: { contents: ( @@ -283,19 +275,20 @@ const FileSystemItem = ({ (itemOperation) => allMenuItems[itemOperation] ) - const [, dragMoveRef, preview] = useDrag(() => - ({ type: DragTypes.MOVABLE_FILE, - item: () => { - if (selected.findIndex(item => item.cid === file.cid && item.name === file.name) >= 0) { - return { selected: selected } - } else { - return { selected: [...selected, { - cid: file.cid, - name: file.name - }] } - } + const [, dragMoveRef, preview] = useDrag({ + type: DragTypes.MOVABLE_FILE, + canDrag: !editingFile, + item: () => { + if (selected.findIndex(item => item.cid === file.cid && item.name === file.name) >= 0) { + return { selected: selected } + } else { + return { selected: [...selected, { + cid: file.cid, + name: file.name + }] } } - }), [selected]) + } + }) useEffect(() => { // This gets called after every render, by default @@ -311,17 +304,21 @@ const FileSystemItem = ({ const [{ isOverMove }, dropMoveRef] = useDrop({ accept: DragTypes.MOVABLE_FILE, - canDrop: () => isFolder, + canDrop: (item) => isFolder && + item.selected.findIndex((s) => s.cid === file.cid && s.name === file.name) < 0, drop: (item: {selected: ISelectedFile[]}) => { moveItems && moveItems(item.selected, getPathWithFile(currentPath, name)) + resetSelectedFiles() }, collect: (monitor) => ({ - isOverMove: monitor.isOver() + isOverMove: monitor.isOver() && + monitor.getItem<{selected: ISelectedFile[]}>().selected.findIndex((s) => s.cid === file.cid && s.name === file.name) < 0 }) }) const [{ isOverUpload }, dropUploadRef] = useDrop({ accept: [NativeTypes.FILE], + canDrop: () => isFolder, drop: (item: any) => { handleUploadOnDrop && handleUploadOnDrop(item.files, item.items, getPathWithFile(currentPath, name)) @@ -333,12 +330,20 @@ const FileSystemItem = ({ const fileOrFolderRef = useRef() - if (!editingFile && isFolder) { - dropMoveRef(fileOrFolderRef) - dropUploadRef(fileOrFolderRef) + if (fileOrFolderRef?.current) { + if (editingFile) { + fileOrFolderRef.current.draggable = false + } else { + fileOrFolderRef.current.draggable = true + } } - if (!editingFile && !isFolder) { + + if (!editingFile && desktop) { dragMoveRef(fileOrFolderRef) + if (isFolder) { + dropMoveRef(fileOrFolderRef) + dropUploadRef(fileOrFolderRef) + } } const onFilePreview = useCallback(() => { @@ -402,6 +407,8 @@ const FileSystemItem = ({ click(e) } + const Icon = getIconForItem(file) + const itemProps = { ref: fileOrFolderRef, currentPath, @@ -437,7 +444,8 @@ const FileSystemItem = ({ inner: classes.modalInner }} closePosition="none" - onClose={() => setEditingFile(undefined)} + active={!!editingFile} + onClose={stopEditing} >
diff --git a/packages/storage-ui/src/Components/Modules/FilesList/DragPreviewLayer.tsx b/packages/storage-ui/src/Components/Modules/FilesList/DragPreviewLayer.tsx new file mode 100644 index 0000000000..39a3544aa8 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/FilesList/DragPreviewLayer.tsx @@ -0,0 +1,193 @@ +import { FolderFilledSvg, FileImageSvg, FilePdfSvg, FileTextSvg, Typography } from "@chainsafe/common-components" +import { makeStyles } from "@chainsafe/common-theme" +import clsx from "clsx" +import React from "react" +import { useDragLayer, XYCoord } from "react-dnd" +import { FileSystemItem } from "../../../Contexts/StorageContext" +import { CSSTheme } from "../../../Themes/types" +import { DragTypes } from "./DragConstants" +import { BrowserView } from "../../../Contexts/types" +import { ISelectedFile } from "../../../Contexts/FileBrowserContext" + +const useStyles = makeStyles(({ breakpoints, constants, palette }: CSSTheme) => { + return ({ + rowItem: { + display: "flex", + height: 70, + border: "2px solid transparent" + }, + fileIcon: { + "& svg": { + width: constants.generalUnit * 2.5, + fill: constants.fileSystemItemRow.icon + }, + [breakpoints.up("md")]: { + paddingLeft: constants.generalUnit * 8.5 + }, + paddingRight: constants.generalUnit * 6 + }, + folderIcon: { + "& svg": { + fill: palette.additional.gray[9] + } + }, + filename: { + whiteSpace: "nowrap", + textOverflow: "ellipsis", + overflow: "hidden" + }, + previewDragLayer: { + position: "fixed", + pointerEvents: "none", + zIndex: 10000, + left: 0, + top: 0, + bottom: 0, + right: 0 + }, + gridViewContainer: { + display: "flex", + flex: 1, + maxWidth: constants.generalUnit * 24 + }, + gridFolderName: { + textAlign: "center", + wordBreak: "break-all", + overflowWrap: "break-word", + padding: constants.generalUnit + }, + gridViewIconNameBox: { + display: "flex", + flexDirection: "column", + width: "100%", + cursor: "pointer" + }, + gridIcon: { + display: "flex", + justifyContent: "center", + alignItems: "center", + height: constants.generalUnit * 16, + maxWidth: constants.generalUnit * 24, + border: `1px solid ${palette.additional["gray"][6]}`, + boxShadow: constants.filesTable.gridItemShadow, + "& svg": { + width: "30%" + }, + [breakpoints.down("lg")]: { + height: constants.generalUnit * 16 + }, + [breakpoints.down("sm")]: { + height: constants.generalUnit * 16 + } + }, + menuTitleGrid: { + padding: `0 ${constants.generalUnit * 0.5}px`, + [breakpoints.down("md")]: { + padding: 0 + } + } + })}) + +const DragPreviewRowItem: React.FC<{item: FileSystemItem; icon: React.ReactNode}> = ({ + item: { name, isFolder }, + icon +}) => { + const classes = useStyles() + return ( +
+
+ {icon} +
+
+ {name} +
+
+ ) +} + +const DragPreviewGridItem: React.FC<{item: FileSystemItem; icon: React.ReactNode}> = ({ + item: { name, isFolder }, + icon +}) => { + const classes = useStyles() + return ( +
+
+
+ {icon} +
+
{name}
+
+
+ ) +} + +export const DragPreviewLayer: React.FC<{items: FileSystemItem[]; previewType: BrowserView} > = ({ items, previewType }) => { + const classes = useStyles() + const { isDragging, dragItems, itemType, currentOffset } = useDragLayer(monitor => ({ + itemType: monitor.getItemType(), + dragItems: monitor.getItem() as {selected: ISelectedFile[]}, + isDragging: monitor.isDragging(), + initialOffset: monitor.getInitialSourceClientOffset(), + currentOffset: monitor.getSourceClientOffset() + })) + + const getItemStyles = (currentOffset: XYCoord | null) => { + if (!currentOffset) { + return { + display: "none" + } + } + const { x, y } = currentOffset + + const transform = `translate(${x}px, ${y}px)` + return { + transform, + WebkitTransform: transform + } + } + + return (!isDragging || itemType !== DragTypes.MOVABLE_FILE) + ? null + :
+
    + {dragItems.selected.map(di => { + const previewItem = items.find(i => i.cid === di.cid && i.name === di.name) + + if (previewItem) { + let Icon + if (previewItem.isFolder) { + Icon = FolderFilledSvg + } else if (previewItem.content_type.includes("image")) { + Icon = FileImageSvg + } else if (previewItem.content_type.includes("pdf")) { + Icon = FilePdfSvg + } else { + Icon = FileTextSvg + } + + return (previewType === "table") + ? } + key={`${previewItem.cid}_${previewItem.name}`} + /> + : } + key={`${previewItem.cid}_${previewItem.name}`} + /> + } else { + return null + }})} +
+
+} diff --git a/packages/storage-ui/src/Components/Modules/FilesList/FilesList.tsx b/packages/storage-ui/src/Components/Modules/FilesList/FilesList.tsx index 86161c6556..929c460b9e 100644 --- a/packages/storage-ui/src/Components/Modules/FilesList/FilesList.tsx +++ b/packages/storage-ui/src/Components/Modules/FilesList/FilesList.tsx @@ -1,5 +1,5 @@ import { createStyles, makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" -import React, { useCallback, useEffect } from "react" +import React, { useCallback, useEffect, useRef } from "react" import { MenuDropdown, PlusIcon, @@ -46,6 +46,9 @@ import { ISelectedFile, useFileBrowser } from "../../../Contexts/FileBrowserCont import SurveyBanner from "../SurveyBanner" import { useStorageApi } from "../../../Contexts/StorageApiContext" import RestrictedModeBanner from "../../Elements/RestrictedModeBanner" +import { DragPreviewLayer } from "./DragPreviewLayer" +import FolderBreadcrumb from "./FolderBreadcrumb" +import { DragTypes } from "./DragConstants" interface IStyleProps { themeKey: string @@ -323,6 +326,7 @@ const FilesList = () => { renameItem: handleRename, deleteItems, viewFolder, + moveItems, currentPath, refreshContents, loadingCurrentPath, @@ -453,6 +457,35 @@ const FilesList = () => { }) }) + const [{ isOverUploadHomeBreadcrumb }, dropUploadHomeBreadcrumbRef] = useDrop({ + accept: [NativeTypes.FILE], + drop: (item: { + files: File[] + items:DataTransferItemList + }) => { + handleUploadOnDrop && handleUploadOnDrop(item.files, item.items, "/") + }, + collect: (monitor) => ({ + isOverUploadHomeBreadcrumb: monitor.isOver() + }) + }) + + const [{ isOverMoveHomeBreadcrumb }, dropMoveHomeBreadcrumbRef] = useDrop({ + accept: DragTypes.MOVABLE_FILE, + canDrop: () => currentPath !== "/", + drop: (item: {selected: ISelectedFile[]}) => { + moveItems && moveItems(item.selected, "/") + resetSelectedCids() + }, + collect: (monitor) => ({ + isOverMoveHomeBreadcrumb: monitor.isOver() && currentPath !== "/" + }) + }) + + const homeBreadcrumbRef = useRef(null) + dropMoveHomeBreadcrumbRef(homeBreadcrumbRef) + dropUploadHomeBreadcrumbRef(homeBreadcrumbRef) + // Modals const [createFolderModalOpen, setCreateFolderModalOpen] = useState(false) const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) @@ -604,11 +637,34 @@ const FilesList = () => { Drop to upload files +
{crumbs && moduleRootPath && ( ({ + ...crumb, + component: (i < crumbs.length - 1) + ? { + console.log(item, crumb.path) + moveItems && crumb.path && moveItems(item.selected, crumb.path) + resetSelectedCids() + }} + handleUpload={(item) => handleUploadOnDrop && + crumb.path && + handleUploadOnDrop(item.files, item.items, crumb.path) + } + /> + : null + }))} homeOnClick={() => redirect(moduleRootPath)} + homeRef={homeBreadcrumbRef} + homeActive={isOverUploadHomeBreadcrumb || isOverMoveHomeBreadcrumb} showDropDown={!desktop} /> )} diff --git a/packages/storage-ui/src/Components/Modules/FilesList/FolderBreadcrumb.tsx b/packages/storage-ui/src/Components/Modules/FilesList/FolderBreadcrumb.tsx new file mode 100644 index 0000000000..c096005f05 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/FilesList/FolderBreadcrumb.tsx @@ -0,0 +1,85 @@ +import { Typography } from "@chainsafe/common-components" +import { createStyles, makeStyles } from "@material-ui/core" +import clsx from "clsx" +import React, { useRef } from "react" +import { useDrop } from "react-dnd" +import { NativeTypes } from "react-dnd-html5-backend" +import { CSSTheme } from "../../../Themes/types" +import { DragTypes } from "./DragConstants" + +const useStyles = makeStyles( + ({ palette }: CSSTheme) => { + return createStyles({ + crumb: { + fontSize: 14, + display: "inline-block", + cursor: "pointer", + maxWidth: 120, + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis" + }, + wrapper: { + border: "1px solid transparent", + padding: "0 4px", + "&.active": { + borderColor: palette.primary.main + } + }, + fullWidth: { + width: "100%" + } + }) + } +) + +interface IFolderBreadcrumb { + onClick?: () => void + folderName: string + handleMove: (item: any) => void + handleUpload: (item: any) => void +} + +const FolderBreadcrumb = ({ onClick, folderName, handleMove, handleUpload }: IFolderBreadcrumb) => { + const classes = useStyles() + + const [{ isOverUploadFolderBreadcrumb }, dropUploadFolderBreadcrumbRef] = useDrop({ + accept: [NativeTypes.FILE], + drop: handleUpload, + collect: (monitor) => ({ + isOverUploadFolderBreadcrumb: monitor.isOver() + }) + }) + + const [{ isOverMoveFolderBreadcrumb }, dropMoveFolderBreadcrumbRef] = useDrop({ + accept: DragTypes.MOVABLE_FILE, + drop: handleMove, + collect: (monitor) => ({ + isOverMoveFolderBreadcrumb: monitor.isOver() + }) + }) + + const folderBreadcrumbRef = useRef(null) + dropMoveFolderBreadcrumbRef(folderBreadcrumbRef) + dropUploadFolderBreadcrumbRef(folderBreadcrumbRef) + + return ( +
+ + {folderName} + +
+ ) +} + +export default FolderBreadcrumb \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Pages/BucketPage.tsx b/packages/storage-ui/src/Components/Pages/BucketPage.tsx index 9ef4483be4..e7d137cb9b 100644 --- a/packages/storage-ui/src/Components/Pages/BucketPage.tsx +++ b/packages/storage-ui/src/Components/Pages/BucketPage.tsx @@ -6,7 +6,8 @@ import { getURISafePathFromArray, getPathWithFile, extractFileBrowserPathFromURL, - getUrlSafePathWithFile + getUrlSafePathWithFile, + joinArrayOfPaths } from "../../Utils/pathUtils" import { IBulkOperations, IFileBrowserModuleProps, IFilesTableBrowserProps } from "../../Contexts/types" import FilesList from "../Modules/FilesList/FilesList" @@ -153,14 +154,17 @@ const BucketPage: React.FC = () => { // Breadcrumbs/paths const arrayOfPaths = useMemo(() => getArrayOfPaths(currentPath), [currentPath]) - const crumbs: Crumb[] = useMemo(() => arrayOfPaths.map((path, index) => ({ - text: decodeURIComponent(path), - onClick: () => { - redirect( - ROUTE_LINKS.Bucket(bucketId, getURISafePathFromArray(arrayOfPaths.slice(0, index + 1))) - ) - } - })), [arrayOfPaths, bucketId, redirect]) + + const crumbs: Crumb[] = useMemo(() => arrayOfPaths.map((path, index) => { + return { + text: decodeURIComponent(path), + onClick: () => { + redirect( + ROUTE_LINKS.Bucket(bucketId, getURISafePathFromArray(arrayOfPaths.slice(0, index + 1))) + ) + }, + path: joinArrayOfPaths(arrayOfPaths.slice(0, index + 1)) + }}), [arrayOfPaths, redirect, bucketId]) const currentFolder = useMemo(() => { return !!arrayOfPaths.length && arrayOfPaths[arrayOfPaths.length - 1] diff --git a/packages/storage-ui/src/Utils/getItemIcon.ts b/packages/storage-ui/src/Utils/getItemIcon.ts new file mode 100644 index 0000000000..bab7a74d4e --- /dev/null +++ b/packages/storage-ui/src/Utils/getItemIcon.ts @@ -0,0 +1,15 @@ +import { FileAudioIcon, FileIcon, FileImageIcon, FilePdfIcon, FileTextIcon, FileVideoIcon, FolderIcon } from "@chainsafe/common-components" +import { FileSystemItem } from "../Contexts/StorageContext" +import { matcher } from "./MimeMatcher" + +export const getIconForItem = (item: FileSystemItem) => { + if (item.isFolder) return FolderIcon + + if (matcher(["image/*"])(item.content_type)) return FileImageIcon + if (matcher(["text/*"])(item.content_type)) return FileTextIcon + if (matcher(["application/pdf"])(item.content_type)) return FilePdfIcon + if (matcher(["audio/*"])(item.content_type)) return FileAudioIcon + if (matcher(["video/*"])(item.content_type)) return FileVideoIcon + + return FileIcon +} diff --git a/packages/storage-ui/src/Utils/pathUtils.ts b/packages/storage-ui/src/Utils/pathUtils.ts index a9a24d88b3..be16f0fbf3 100644 --- a/packages/storage-ui/src/Utils/pathUtils.ts +++ b/packages/storage-ui/src/Utils/pathUtils.ts @@ -20,6 +20,15 @@ export function getArrayOfPaths(path: string): string[] { } } +// [] -> "/" +// ["path", "to", "this"] => "/path/to/this" +export function joinArrayOfPaths(arrayOfPaths: string[]): string { + if (!arrayOfPaths.length) return "/" + else { + return `/${arrayOfPaths.join("/")}` + } +} + // [] -> "/" // ["path", "to", "this"] -> "/path/to/this" export function getURISafePathFromArray(arrayOfPaths: string[]): string {