From ac96f3d24b723de2c3d830d16db742bef980f3be Mon Sep 17 00:00:00 2001 From: Maksim Chervonnyi Date: Wed, 23 Aug 2023 17:36:43 +0300 Subject: [PATCH] Download and bookmark on listing item mouseover (#3697) * Download and bookmark on listing item mouseover * use physicalKey for package files * fix passing physicalKey * add snapshot unit tests * there is path always, because we download something inside package * throw error on unexpected * cleaner mock for URL * throw instead of assertNever --- catalog/app/containers/Bucket/Dir.tsx | 13 +- catalog/app/containers/Bucket/Download.tsx | 11 +- catalog/app/containers/Bucket/FileView.js | 11 +- catalog/app/containers/Bucket/Listing.tsx | 29 +- .../containers/Bucket/ListingActions.spec.tsx | 103 +++++++ .../app/containers/Bucket/ListingActions.tsx | 266 ++++++++++++++++ .../Bucket/PackageTree/PackageTree.tsx | 6 +- .../Bucket/__mocks__/react-redux.ts | 1 + .../containers/Bucket/__mocks__/utils/AWS.ts | 5 + .../ListingActions.spec.tsx.snap | 286 ++++++++++++++++++ catalog/app/embed/Dir.js | 9 +- docs/CHANGELOG.md | 1 + 12 files changed, 720 insertions(+), 21 deletions(-) create mode 100644 catalog/app/containers/Bucket/ListingActions.spec.tsx create mode 100644 catalog/app/containers/Bucket/ListingActions.tsx create mode 100644 catalog/app/containers/Bucket/__mocks__/react-redux.ts create mode 100644 catalog/app/containers/Bucket/__mocks__/utils/AWS.ts create mode 100644 catalog/app/containers/Bucket/__snapshots__/ListingActions.spec.tsx.snap diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index a4eaee710a6..dc8ee877f9f 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -362,11 +362,14 @@ export default function Dir() { prefs, )} {!cfg.noDownload && !cfg.desktop && ( - + + + )} diff --git a/catalog/app/containers/Bucket/Download.tsx b/catalog/app/containers/Bucket/Download.tsx index 84f5c5a0abc..c7093f8a22f 100644 --- a/catalog/app/containers/Bucket/Download.tsx +++ b/catalog/app/containers/Bucket/Download.tsx @@ -29,7 +29,16 @@ export function DownloadButton({ className, label, onClick, path }: DownloadButt ) } - return + return ( + + + + ) } const STORAGE_KEYS = { diff --git a/catalog/app/containers/Bucket/FileView.js b/catalog/app/containers/Bucket/FileView.js index 7d2e009d0e6..19bdacf18a1 100644 --- a/catalog/app/containers/Bucket/FileView.js +++ b/catalog/app/containers/Bucket/FileView.js @@ -48,24 +48,21 @@ export function ViewModeSelector({ className, ...props }) { ) } -export function ZipDownloadForm({ className, suffix, label, newTab = false }) { +/** Child button must have `type="submit"` */ +export function ZipDownloadForm({ className = '', suffix, children, newTab = false }) { const { token } = redux.useSelector(tokensSelector) || {} if (!token || cfg.noDownload) return null const action = `${cfg.s3Proxy}/zip/${suffix}` return (
- + {children} ) } diff --git a/catalog/app/containers/Bucket/Listing.tsx b/catalog/app/containers/Bucket/Listing.tsx index 986abc858d4..6b7fbdd829e 100644 --- a/catalog/app/containers/Bucket/Listing.tsx +++ b/catalog/app/containers/Bucket/Listing.tsx @@ -17,6 +17,8 @@ import { readableBytes } from 'utils/string' import * as tagged from 'utils/taggedV2' import usePrevious from 'utils/usePrevious' +import { RowActions } from './ListingActions' + const EMPTY = {''} const TIP_DELAY = 1000 @@ -33,7 +35,13 @@ export interface Item { } export const Entry = tagged.create('app/containers/Listing:Entry' as const, { - File: (f: { key: string; size?: number; archived?: boolean; modified?: Date }) => f, + File: (f: { + archived?: boolean + key: string + modified?: Date + physicalKey?: string + size?: number + }) => f, Dir: (d: { key: string; size?: number }) => d, }) @@ -105,11 +113,12 @@ export function format( to: toDir(key), size, }), - File: ({ key, size, archived, modified }) => ({ + File: ({ key, size, archived, modified, physicalKey }) => ({ type: 'file' as const, name: s3paths.withoutPrefix(prefix, key), to: toFile(key), size, + physicalKey, modified, archived, }), @@ -1152,6 +1161,22 @@ export function Listing({ }, }) } + columnsWithValues.push({ + field: 'actions', + headerName: '', + align: 'right', + width: 0, + renderCell: (params: DG.GridCellParams) => + params.id === '..' ? ( + <> + ) : ( + + ), + }) return columnsWithValues }, [classes, CellComponent, items, sm]) diff --git a/catalog/app/containers/Bucket/ListingActions.spec.tsx b/catalog/app/containers/Bucket/ListingActions.spec.tsx new file mode 100644 index 00000000000..8dca39c20c9 --- /dev/null +++ b/catalog/app/containers/Bucket/ListingActions.spec.tsx @@ -0,0 +1,103 @@ +import * as React from 'react' +import renderer from 'react-test-renderer' +import * as NamedRoutes from 'utils/NamedRoutes' +import * as Bookmarks from 'containers/Bookmarks/Provider' +import { bucketFile, bucketDir, bucketPackageTree } from 'constants/routes' + +import { RowActions } from './ListingActions' + +function TestBucket({ children }: React.PropsWithChildren<{}>) { + return ( + + + {children} + + + ) +} + +describe('components/ListingActions', () => { + describe('RowActions', () => { + it('should render nothing if archived', () => { + const tree = renderer + .create( + + + , + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render nothing if no route', () => { + const tree = renderer + .create( + + + , + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render nothing if wrong route', () => { + const tree = renderer + .create( + + + , + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render Bucket directory', () => { + jest.mock('react-redux') + const tree = renderer + .create( + + + , + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render Bucket file', () => { + jest.mock('utils/AWS') + const tree = renderer + .create( + + + , + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render Package directory', () => { + const tree = renderer + .create( + + + , + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render Package file', () => { + const tree = renderer + .create( + + + , + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) + }) +}) diff --git a/catalog/app/containers/Bucket/ListingActions.tsx b/catalog/app/containers/Bucket/ListingActions.tsx new file mode 100644 index 00000000000..08f47d2f467 --- /dev/null +++ b/catalog/app/containers/Bucket/ListingActions.tsx @@ -0,0 +1,266 @@ +import cx from 'classnames' +import * as React from 'react' +import { matchPath, match as Match } from 'react-router-dom' +import * as M from '@material-ui/core' + +import * as Bookmarks from 'containers/Bookmarks/Provider' +import * as Model from 'model' +import * as AWS from 'utils/AWS' +import * as NamedRoutes from 'utils/NamedRoutes' +import * as s3paths from 'utils/s3paths' + +import * as FileView from './FileView' + +const useButtonStyles = M.makeStyles({ + root: { + padding: '5px', + }, +}) + +interface ButtonProps extends M.IconButtonProps { + download?: boolean + href?: string + icon: string +} + +function Button({ icon, className, ...props }: ButtonProps) { + const classes = useButtonStyles() + return ( + + {icon} + + ) +} + +interface BucketButtonProps { + className: string + location: Model.S3.S3ObjectLocation +} + +function Bookmark({ className, location }: BucketButtonProps) { + const bookmarks = Bookmarks.use() + const isBookmarked = location ? bookmarks.isBookmarked('main', location) : false + const toggleBookmark = () => location && bookmarks.toggle('main', location) + return ( + +
+ + +
+ + + +`; + +exports[`components/ListingActions RowActions should render Bucket file 1`] = ` +
+
+
+ + + + + arrow_downward + + + + +
+
+
+`; + +exports[`components/ListingActions RowActions should render Package directory 1`] = ` +
+
+
+
+ + +
+
+
+
+`; + +exports[`components/ListingActions RowActions should render Package file 1`] = ` + +`; + +exports[`components/ListingActions RowActions should render nothing if archived 1`] = `null`; + +exports[`components/ListingActions RowActions should render nothing if no route 1`] = `null`; + +exports[`components/ListingActions RowActions should render nothing if wrong route 1`] = `null`; diff --git a/catalog/app/embed/Dir.js b/catalog/app/embed/Dir.js index 1f872016ee6..bb92054cb0d 100644 --- a/catalog/app/embed/Dir.js +++ b/catalog/app/embed/Dir.js @@ -6,6 +6,7 @@ import { useHistory, useLocation, useParams } from 'react-router-dom' import * as M from '@material-ui/core' import * as BreadCrumbs from 'components/BreadCrumbs' +import * as Buttons from 'components/Buttons' import cfg from 'constants/config' import * as AWS from 'utils/AWS' import AsyncResult from 'utils/AsyncResult' @@ -123,11 +124,9 @@ export default function Dir() { {!cfg.noDownload && ( - + + + )} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 713670d6286..0a474ae70e5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -28,6 +28,7 @@ Entries inside each section should be ordered by type: * [Added] Add links to documentation and re-use code samples ([#3496](https://github.com/quiltdata/quilt/pull/3496)) * [Added] Show S3 Object tags ([#3515](https://github.com/quiltdata/quilt/pull/3515)) * [Added] Indexer lambda now indexes S3 Object tags ([#3691](https://github.com/quiltdata/quilt/pull/3691)) +* [Added] Add download and bookmarks button to file listings ([#3697](https://github.com/quiltdata/quilt/pull/3697)) * [Changed] Enable user selection in perspective grids ([#3453](https://github.com/quiltdata/quilt/pull/3453)) * [Changed] Hide columns without values in files listings ([#3512](https://github.com/quiltdata/quilt/pull/3512)) * [Changed] Enable `allow-same-origin` for iframes in browsable buckets ([#3516](https://github.com/quiltdata/quilt/pull/3516))