diff --git a/packages/common-components/src/Breadcrumb/Breadcrumb.tsx b/packages/common-components/src/Breadcrumb/Breadcrumb.tsx index 5bcf51c909..e6072e3aa8 100644 --- a/packages/common-components/src/Breadcrumb/Breadcrumb.tsx +++ b/packages/common-components/src/Breadcrumb/Breadcrumb.tsx @@ -7,13 +7,19 @@ import { MenuDropdown } from "../MenuDropdown" export type Crumb = { text: string + path?: string onClick?: () => void + forwardedRef?: React.Ref + active?: boolean + component?: React.ReactElement | null } export type BreadcrumbProps = { crumbs?: Crumb[] homeOnClick?: () => void hideHome?: boolean + homeRef?: React.Ref + homeActive?: boolean className?: string showDropDown?: boolean } @@ -36,6 +42,7 @@ const useStyles = makeStyles( }, home: { height: 16, + width: "fit-content", margin: "3px 0", "& > svg": { display: "block", @@ -79,40 +86,65 @@ const useStyles = makeStyles( textOverflow: "ellipsis" }, menuTitle: { - padding: `0px ${constants.generalUnit}px 0px 0px` + padding: `0px ${constants.generalUnit * 1.5}px 0px ${constants.generalUnit * 0.5}px` }, menuIcon: { width: 12, height: 12, "& svg": { height: 12, - width: 12 + width: 12, + fill: palette.additional["gray"][9] } + }, + wrapper: { + border: "1px solid transparent", + padding: `0 ${constants.generalUnit * 0.5}px`, + "&.active": { + borderColor: palette.primary.main + } + }, + fullWidth: { + width: "100%" } }) ) -const Breadcrumb: React.FC = ({ +const CrumbComponent = ({ crumb }: {crumb: Crumb}) => { + const classes = useStyles() + return ( + !crumb.component + ?
+ crumb.onClick && crumb.onClick()} + className={clsx(classes.crumb, crumb.onClick && "clickable")} + variant="body1" + > + {crumb.text} + +
+ : crumb.component + ) +} + +const Breadcrumb = ({ crumbs = [], homeOnClick, hideHome, + homeRef, + homeActive, className, showDropDown }: BreadcrumbProps) => { const classes = useStyles() const generateFullCrumbs = (crumbs: Crumb[]) => { - return crumbs.map((item: Crumb, index: number) => ( + return crumbs.map((crumb: Crumb, index: number) => ( -
- (item.onClick ? item.onClick() : null)} - className={clsx(classes.crumb, item.onClick && "clickable")} - variant="body1" - > - {item.text} - -
+ {index < (crumbs.length - 1) &&
} )) @@ -131,14 +163,7 @@ const Breadcrumb: React.FC = ({ titleText: classes.menuTitleText }} menuItems={crumbs.map((crumb) => ({ - contents: ( - (crumb.onClick ? crumb.onClick() : null)} - > - {crumb.text} - - ) + contents: }))} /> ) @@ -154,15 +179,7 @@ const Breadcrumb: React.FC = ({ <> {generateDropdownCrumb(dropdownCrumbs)}
-
- (lastCrumb.onClick ? lastCrumb.onClick() : null)} - className={clsx(classes.crumb, lastCrumb.onClick && "clickable")} - variant="body1" - > - {lastCrumb.text} - -
+ ) } @@ -171,11 +188,16 @@ const Breadcrumb: React.FC = ({ return (
{!hideHome && <> - (homeOnClick ? homeOnClick() : null)} - /> -
+
+ homeOnClick && homeOnClick()} + /> +
+ {!!crumbs.length &&
} } {generateCrumbs()} diff --git a/packages/common-components/src/stories/Breadcrumb.stories.tsx b/packages/common-components/src/stories/Breadcrumb.stories.tsx index ba9be0c287..46db1603d6 100644 --- a/packages/common-components/src/stories/Breadcrumb.stories.tsx +++ b/packages/common-components/src/stories/Breadcrumb.stories.tsx @@ -19,7 +19,8 @@ export const BreadcrumbStory = (): React.ReactNode => { const crumbs = useMemo(() => ([ { text: text("breadcrumb 2", "Level 1 Clickable"), - onClick: () => actionsData.linkClick() + onClick: () => actionsData.linkClick(), + active: boolean("breadcrumb-2-active", false) }, { text: "Level 2" @@ -36,6 +37,7 @@ export const BreadcrumbStory = (): React.ReactNode => { <> actionsData.homeClicked()} + homeActive={boolean("home-active", false)} showDropDown={boolean("show dropdown", true)} crumbs={crumbs} hideHome={boolean("hide home", false)} diff --git a/packages/common-theme/rollup.config.js b/packages/common-theme/rollup.config.js index 2e31c6e0d3..88739398c5 100644 --- a/packages/common-theme/rollup.config.js +++ b/packages/common-theme/rollup.config.js @@ -5,7 +5,7 @@ import json from "@rollup/plugin-json" import nodePolyfills from "rollup-plugin-node-polyfills" import peerDepsExternal from "rollup-plugin-peer-deps-external" import postcss from "rollup-plugin-postcss" -import copy from 'rollup-plugin-copy' +import copy from "rollup-plugin-copy" export default { input: "src/index.ts", diff --git a/packages/files-ui/package.json b/packages/files-ui/package.json index c0e4f9b470..7b8c7ccc64 100644 --- a/packages/files-ui/package.json +++ b/packages/files-ui/package.json @@ -84,7 +84,7 @@ "scripts": { "postinstall": "yarn compile", "start": "yarn compile && craco --max_old_space_size=4096 start", - "build": "craco --max_old_space_size=4096 --openssl-legacy-provider build ", + "build": "craco --max_old_space_size=4096 --openssl-legacy-provider build", "sentry": "(export REACT_APP_SENTRY_RELEASE=$(sentry-cli releases propose-version); node scripts/sentry.js)", "release": "(export REACT_APP_SENTRY_RELEASE=$(sentry-cli releases propose-version); yarn compile && yarn build && node scripts/sentry.js)", "test": "cypress open", diff --git a/packages/files-ui/src/App.tsx b/packages/files-ui/src/App.tsx index 6b18cba351..5301d37c85 100644 --- a/packages/files-ui/src/App.tsx +++ b/packages/files-ui/src/App.tsx @@ -17,6 +17,13 @@ import { UserProvider } from "./Contexts/UserContext" import { BillingProvider } from "./Contexts/BillingContext" import { PosthogProvider } from "./Contexts/PosthogContext" import { NotificationsProvider } from "./Contexts/NotificationsContext" +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" && @@ -94,57 +101,57 @@ const App = () => { ), []) return ( - - - window.location.reload()} + + - - - - window.location.reload()} + > + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx index 133088fe5a..6d0847287a 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx @@ -8,7 +8,8 @@ import { extractFileBrowserPathFromURL, getUrlSafePathWithFile, getAbsolutePathsFromCids, - pathEndingWithSlash + pathEndingWithSlash, + joinArrayOfPaths } from "../../../Utils/pathUtils" import { IBulkOperations, IFileBrowserModuleProps, IFilesTableBrowserProps } from "./types" import FilesList from "./views/FilesList" @@ -145,14 +146,16 @@ const CSFFileBrowser: 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.Drive(getURISafePathFromArray(arrayOfPaths.slice(0, index + 1))) - ) - } - })), [arrayOfPaths, redirect]) + const crumbs: Crumb[] = useMemo(() => arrayOfPaths.map((path, index) => { + return { + text: decodeURIComponent(path), + onClick: () => { + redirect( + ROUTE_LINKS.Drive(getURISafePathFromArray(arrayOfPaths.slice(0, index + 1))) + ) + }, + path: joinArrayOfPaths(arrayOfPaths.slice(0, index + 1)) + }}), [arrayOfPaths, redirect]) const handleUploadOnDrop = useCallback(async (files: File[], fileItems: DataTransferItemList, path: string) => { if (!bucket) return 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 f86bfafdb4..64b04f3dae 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 @@ -381,17 +381,17 @@ const FileSystemItem = ({ (itemOperation) => allMenuItems[itemOperation] ) - const [, dragMoveRef, preview] = useDrag(() => - ({ - type: DragTypes.MOVABLE_FILE, - item: () => { - if (selectedCids.includes(file.cid)) { - return { ids: selectedCids } - } else { - return { ids: [...selectedCids, file.cid] } - } + const [, dragMoveRef, preview] = useDrag({ + type: DragTypes.MOVABLE_FILE, + canDrag: !isFolder, + item: () => { + if (selectedCids.includes(file.cid)) { + return { ids: selectedCids } + } else { + return { ids: [...selectedCids, file.cid] } } - }), [selectedCids]) + } + }) useEffect(() => { // This gets called after every render, by default @@ -418,6 +418,7 @@ const FileSystemItem = ({ const [{ isOverUpload }, dropUploadRef] = useDrop({ accept: [NativeTypes.FILE], + canDrop: () => isFolder, drop: (item: any) => { handleUploadOnDrop && handleUploadOnDrop(item.files, item.items, getPathWithFile(currentPath, name)) @@ -429,13 +430,13 @@ const FileSystemItem = ({ const fileOrFolderRef = useRef() - if (!editing && isFolder) { - dropMoveRef(fileOrFolderRef) - dropUploadRef(fileOrFolderRef) - } - - if (!editing && !isFolder) { - desktop && dragMoveRef(fileOrFolderRef) + if (!editing && desktop) { + if (isFolder) { + dropMoveRef(fileOrFolderRef) + dropUploadRef(fileOrFolderRef) + } else { + dragMoveRef(fileOrFolderRef) + } } const onSingleClick = useCallback( diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx index 8f691586bf..48d2b59e56 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/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 { Divider, PlusIcon, @@ -57,6 +57,8 @@ import { useSharingExplainerModalFlag } from "../hooks/useSharingExplainerModalF import { ListItemIcon, ListItemText } from "@material-ui/core" import { useFilesApi } from "../../../../Contexts/FilesApiContext" import RestrictedModeBanner from "../../../Elements/RestrictedModeBanner" +import { DragTypes } from "../DragConstants" +import FolderBreadcrumb from "./FolderBreadcrumb" const baseOperations: FileOperation[] = ["download", "info", "preview", "share"] const readerOperations: FileOperation[] = [...baseOperations, "report"] @@ -341,6 +343,7 @@ const FilesList = ({ isShared = false }: Props) => { handleUploadOnDrop, bulkOperations, crumbs, + moveItems, renameItem: handleRename, deleteItems: deleteFiles, viewFolder, @@ -491,7 +494,7 @@ const FilesList = ({ isShared = false }: Props) => { } }, [selectedItems.length, items]) - const [{ isOverUploadable, isOverBrowser }, dropBrowserRef] = useDrop({ + const [{ isOverBrowserOnlyAndUploadable, isOverBrowser }, dropBrowserRef] = useDrop({ accept: [NativeTypes.FILE], drop: (item: any, monitor) => { if (monitor.isOver({ shallow: true })) { @@ -503,11 +506,38 @@ const FilesList = ({ isShared = false }: Props) => { }, collect: (monitor) => ({ isOverBrowser: monitor.isOver(), - isOverUploadable: monitor.isOver({ shallow: true }), - canDrop: monitor.canDrop() + isOverBrowserOnlyAndUploadable: monitor.isOver({ shallow: true }) }) }) + 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, + drop: (item: { ids: string[]}) => { + moveItems && moveItems(item.ids, "/") + setSelectedItems([]) + }, + collect: (monitor) => ({ + isOverMoveHomeBreadcrumb: monitor.isOver() + }) + }) + + const homeBreadcrumbRef = useRef(null) + dropMoveHomeBreadcrumbRef(homeBreadcrumbRef) + dropUploadHomeBreadcrumbRef(homeBreadcrumbRef) + // Modals const [createFolderModalOpen, setCreateFolderModalOpen] = useState(false) const [isPreviewOpen, setIsPreviewOpen] = useState(false) @@ -773,7 +803,7 @@ const FilesList = ({ isShared = false }: Props) => { return (
{ > {crumbs && moduleRootPath && ( ({ + ...crumb, + component: (i < crumbs.length - 1) + ? { + moveItems && crumb.path && moveItems(item.ids, crumb.path) + setSelectedItems([]) + }} + 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/files-ui/src/Components/Modules/FileBrowsers/views/FolderBreadcrumb.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FolderBreadcrumb.tsx new file mode 100644 index 0000000000..635dd5495e --- /dev/null +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/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 { CSFTheme } from "../../../../Themes/types" +import { DragTypes } from "../DragConstants" + +const useStyles = makeStyles( + ({ palette }: CSFTheme) => { + 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/files-ui/src/Utils/pathUtils.ts b/packages/files-ui/src/Utils/pathUtils.ts index 0d01539479..72fbaf65b2 100644 --- a/packages/files-ui/src/Utils/pathUtils.ts +++ b/packages/files-ui/src/Utils/pathUtils.ts @@ -22,6 +22,14 @@ 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 {