From 555a8ba3b53c0ca47234603472fb59b8010b4c98 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Wed, 24 Aug 2022 16:55:08 +0300 Subject: [PATCH 01/19] TODO note --- catalog/app/containers/Bucket/Successors.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/catalog/app/containers/Bucket/Successors.tsx b/catalog/app/containers/Bucket/Successors.tsx index fe90fa9bc91..32b514f5a88 100644 --- a/catalog/app/containers/Bucket/Successors.tsx +++ b/catalog/app/containers/Bucket/Successors.tsx @@ -223,6 +223,7 @@ interface ButtonInnerProps { onClick: React.MouseEventHandler } +// TODO: Replace by FileView.AdaptiveButtonLayout function ButtonInner({ children, className, onClick }: ButtonInnerProps) { const t = M.useTheme() const sm = M.useMediaQuery(t.breakpoints.down('sm')) From 155c4aae160787c5becb54d6f9c12d7f0d707088 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Wed, 24 Aug 2022 16:55:43 +0300 Subject: [PATCH 02/19] refactor RevisionMenu --- .../Bucket/PackageTree/RevisionMenu.tsx | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx index 5308a8da608..511fe96ae81 100644 --- a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx +++ b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx @@ -4,40 +4,31 @@ import * as M from '@material-ui/core' import * as BucketPreferences from 'utils/BucketPreferences' import * as Config from 'utils/Config' -interface RevisionMenuProps { - className: string - onDelete: () => void - onDesktop: () => void +interface MenuProps { + className?: string + items: { + onClick: () => void + title: string + }[] } -export default function RevisionMenu({ - className, - onDelete, - onDesktop, -}: RevisionMenuProps) { - const preferences = BucketPreferences.use() - const { desktop }: { desktop: boolean } = Config.use() +function Menu({ className, items }: MenuProps) { const [anchorEl, setAnchorEl] = React.useState(null) const handleOpen = React.useCallback( - (event) => { - setAnchorEl(event.target) - }, + (event) => setAnchorEl(event.target), [setAnchorEl], ) - const handleClose = React.useCallback(() => { - setAnchorEl(null) - }, [setAnchorEl]) + const handleClose = React.useCallback(() => setAnchorEl(null), [setAnchorEl]) - const handleDeleteClick = React.useCallback(() => { - onDelete() - setAnchorEl(null) - }, [onDelete, setAnchorEl]) - const handleDesktopClick = React.useCallback(() => { - onDesktop() - setAnchorEl(null) - }, [onDesktop, setAnchorEl]) + const mkClickHandler = React.useCallback( + (onClick: () => void) => () => { + onClick() + setAnchorEl(null) + }, + [], + ) return ( <> @@ -46,13 +37,46 @@ export default function RevisionMenu({ - {preferences?.ui?.actions?.deleteRevision && ( - Delete revision - )} - {preferences?.ui?.actions?.openInDesktop && !desktop && ( - Open in Teleport - )} + {items.map(({ onClick, title }) => ( + + {title} + + ))} ) } + +interface RevisionMenuProps { + className: string + onDelete: () => void + onDesktop: () => void +} + +export default function RevisionMenu({ + className, + onDelete, + onDesktop, +}: RevisionMenuProps) { + const preferences = BucketPreferences.use() + const { desktop }: { desktop: boolean } = Config.use() + + const items = React.useMemo(() => { + const menu = [] + if (preferences?.ui?.actions?.deleteRevision) { + menu.push({ + onClick: onDelete, + title: 'Delete revision', + }) + } + if (preferences?.ui?.actions?.openInDesktop && !desktop) { + menu.push({ + onClick: onDesktop, + title: 'Open in Teleport', + }) + } + return menu + }, [desktop, onDelete, onDesktop, preferences]) + + return +} From da102d709d6aa4d955a256dceb00d8ab90dbc371 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Wed, 24 Aug 2022 17:02:26 +0300 Subject: [PATCH 03/19] fix styles name --- catalog/app/containers/Bucket/FileView.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/catalog/app/containers/Bucket/FileView.js b/catalog/app/containers/Bucket/FileView.js index a98c1aae4d5..0488a46444f 100644 --- a/catalog/app/containers/Bucket/FileView.js +++ b/catalog/app/containers/Bucket/FileView.js @@ -13,7 +13,7 @@ export * from './Meta' // TODO: move here everything that's reused btw Bucket/File, Bucket/PackageTree and Embed/File -const useDownloadButtonStyles = M.makeStyles((t) => ({ +const useAdaptiveButtonStyles = M.makeStyles((t) => ({ root: { flexShrink: 0, marginBottom: -3, @@ -25,7 +25,7 @@ const useDownloadButtonStyles = M.makeStyles((t) => ({ })) export function AdaptiveButtonLayout({ className, label, icon, ...props }) { - const classes = useDownloadButtonStyles() + const classes = useAdaptiveButtonStyles() const t = M.useTheme() const sm = M.useMediaQuery(t.breakpoints.down('sm')) @@ -64,7 +64,7 @@ export function DownloadButton({ className, handle }) { } export function ViewModeSelector({ className, ...props }) { - const classes = useDownloadButtonStyles() + const classes = useAdaptiveButtonStyles() const t = M.useTheme() const sm = M.useMediaQuery(t.breakpoints.down('sm')) return ( From efa7e7887bccfd7567899c6236a57f0149725277 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Wed, 24 Aug 2022 17:11:15 +0300 Subject: [PATCH 04/19] move Menu to standalone module --- catalog/app/containers/Bucket/Menu.tsx | 46 +++++++++++++++++++ .../Bucket/PackageTree/RevisionMenu.tsx | 44 +----------------- 2 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 catalog/app/containers/Bucket/Menu.tsx diff --git a/catalog/app/containers/Bucket/Menu.tsx b/catalog/app/containers/Bucket/Menu.tsx new file mode 100644 index 00000000000..41500c34472 --- /dev/null +++ b/catalog/app/containers/Bucket/Menu.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' + +import * as M from '@material-ui/core' + +interface MenuProps { + className?: string + items: { + onClick: () => void + title: string + }[] +} + +export default function Menu({ className, items }: MenuProps) { + const [anchorEl, setAnchorEl] = React.useState(null) + + const handleOpen = React.useCallback( + (event) => setAnchorEl(event.target), + [setAnchorEl], + ) + + const handleClose = React.useCallback(() => setAnchorEl(null), [setAnchorEl]) + + const mkClickHandler = React.useCallback( + (onClick: () => void) => () => { + onClick() + setAnchorEl(null) + }, + [], + ) + + return ( + <> + + more_vert + + + + {items.map(({ onClick, title }) => ( + + {title} + + ))} + + + ) +} diff --git a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx index 511fe96ae81..a9b82470256 100644 --- a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx +++ b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx @@ -1,51 +1,9 @@ import * as React from 'react' -import * as M from '@material-ui/core' import * as BucketPreferences from 'utils/BucketPreferences' import * as Config from 'utils/Config' -interface MenuProps { - className?: string - items: { - onClick: () => void - title: string - }[] -} - -function Menu({ className, items }: MenuProps) { - const [anchorEl, setAnchorEl] = React.useState(null) - - const handleOpen = React.useCallback( - (event) => setAnchorEl(event.target), - [setAnchorEl], - ) - - const handleClose = React.useCallback(() => setAnchorEl(null), [setAnchorEl]) - - const mkClickHandler = React.useCallback( - (onClick: () => void) => () => { - onClick() - setAnchorEl(null) - }, - [], - ) - - return ( - <> - - more_vert - - - - {items.map(({ onClick, title }) => ( - - {title} - - ))} - - - ) -} +import Menu from '../Menu' interface RevisionMenuProps { className: string From c6ecb55411bd8b6ba415d92941871c42ce1d673c Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Wed, 24 Aug 2022 17:16:01 +0300 Subject: [PATCH 05/19] dont show menu if nothing to show --- .../containers/Bucket/PackageTree/PackageTree.tsx | 15 +++++---------- .../Bucket/PackageTree/RevisionMenu.tsx | 2 ++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx b/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx index 24f8fa4e18c..0d817fdf119 100644 --- a/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx +++ b/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx @@ -395,9 +395,6 @@ function DirDisplay({ const downloadPath = path ? `package/${bucket}/${name}/${hash}/${path}` : `package/${bucket}/${name}/${hash}` - const hasRevisionMenu = - preferences?.ui?.actions?.deleteRevision || - preferences?.ui?.actions?.openInDesktop // TODO: disable if nothing to revise on desktop const hasReviseButton = preferences?.ui?.actions?.revisePackage @@ -431,13 +428,11 @@ function DirDisplay({ onClick={openInDesktopState.confirm} path={downloadPath} /> - {hasRevisionMenu && ( - - )} + {preferences?.ui?.blocks?.code && ( diff --git a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx index a9b82470256..bf98fb0ced1 100644 --- a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx +++ b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx @@ -36,5 +36,7 @@ export default function RevisionMenu({ return menu }, [desktop, onDelete, onDesktop, preferences]) + if (!items.length) return null + return } From 2a3e985da053f8b57b2e2445f3ad459fe9561f52 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Wed, 24 Aug 2022 17:38:16 +0300 Subject: [PATCH 06/19] add menu to create file --- catalog/app/containers/Bucket/Dir.tsx | 31 ++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index 69173d0a573..3533ef4761f 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -23,11 +23,13 @@ import type * as workflows from 'utils/workflows' import Code from './Code' import * as FileView from './FileView' import { Item, Listing, PrefixFilter } from './Listing' +import Menu from './Menu' import * as PD from './PackageDialog' import * as Successors from './Successors' import Summary from './Summary' import { displayError } from './errors' import * as requests from './requests' +import { createFilterOptions } from '@material-ui/lab' const useAddToBookmarksStyles = M.makeStyles((t) => ({ root: { @@ -105,7 +107,17 @@ function AddToBookmarks({ interface RouteMap { bucketDir: [bucket: string, path?: string, prefix?: string] - bucketFile: [bucket: string, path: string, version?: string] + bucketFile: [ + bucket: string, + path: string, + options?: { + add?: boolean + edit?: boolean + mode?: string + next?: string + version?: string + }, + ] } type Urls = NamedRoutes.Urls @@ -254,6 +266,7 @@ export default function Dir({ const { desktop, noDownload } = Config.use() const s3 = AWS.S3.use() const preferences = BucketPreferences.use() + const history = RRDom.useHistory() const { prefix } = parseSearch(l.search) const path = s3paths.decode(encodedPath) const dest = path ? basename(path) : bucket @@ -330,6 +343,21 @@ export default function Dir({ [packageDirectoryDialog, path], ) + const createFile = React.useCallback(() => { + const name = window.prompt('Enter file name') + if (!name) return + history.push(urls.bucketFile(bucket, join(path, name), { edit: true })) + }, [bucket, history, urls]) + const menuItems = React.useMemo( + () => [ + { + onClick: createFile, + title: 'Create file', + }, + ], + [createFile], + ) + return ( {[path || 'Files', bucket]} @@ -363,6 +391,7 @@ export default function Dir({ label="Download directory" /> )} + {preferences?.ui?.blocks?.code && {code}} From 320e8339dd5ff7a4fb3cc48106b115c931d5aff9 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Wed, 24 Aug 2022 18:10:57 +0300 Subject: [PATCH 07/19] linting --- catalog/app/containers/Bucket/Dir.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index 3533ef4761f..597072ac698 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -29,7 +29,6 @@ import * as Successors from './Successors' import Summary from './Summary' import { displayError } from './errors' import * as requests from './requests' -import { createFilterOptions } from '@material-ui/lab' const useAddToBookmarksStyles = M.makeStyles((t) => ({ root: { @@ -347,7 +346,7 @@ export default function Dir({ const name = window.prompt('Enter file name') if (!name) return history.push(urls.bucketFile(bucket, join(path, name), { edit: true })) - }, [bucket, history, urls]) + }, [bucket, history, path, urls]) const menuItems = React.useMemo( () => [ { From 95756da82ff9a90fea18d94a0e7acfa0905576f6 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 10:54:34 +0300 Subject: [PATCH 08/19] add Prompt Dialog component --- catalog/app/components/Dialog/Prompt.tsx | 58 ++++++++++++++++++++++++ catalog/app/containers/Bucket/Dir.tsx | 1 + 2 files changed, 59 insertions(+) create mode 100644 catalog/app/components/Dialog/Prompt.tsx diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx new file mode 100644 index 00000000000..982c9c2dd9e --- /dev/null +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -0,0 +1,58 @@ +import * as React from 'react' +import * as M from '@material-ui/core' + +interface DialogProps { + onCancel: () => void + onSubmit: (value: string) => void + open: boolean + title: string +} + +// TODO: default value +function Dialog({ open, onCancel, onSubmit, title }: DialogProps) { + const [value, setValue] = React.useState('') + return ( + + {title} + + setValue(e.target.value)} + value={value} + /> + + + + Cancel + + onSubmit(value)} color="primary" variant="contained"> + Submit + + + + ) +} + +interface PromptProps { + title: string + onSubmit: (value: string) => void +} +export function usePrompt({ title, onSubmit }: PromptProps) { + const [opened, setOpened] = React.useState(false) + const render = React.useCallback( + () => , + [close, opened, onSubmit, title], + ) + const open = React.useCallback(() => setOpened(true), []) + const close = React.useCallback(() => setOpened(false), []) + return React.useMemo( + () => ({ + close, + open, + render, + }), + [close, open, render], + ) +} diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index 597072ac698..eef48e204da 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -344,6 +344,7 @@ export default function Dir({ const createFile = React.useCallback(() => { const name = window.prompt('Enter file name') + // TODO: if name endsWith unsupported ext if (!name) return history.push(urls.bucketFile(bucket, join(path, name), { edit: true })) }, [bucket, history, path, urls]) From 011eeb9d08030bb2e25f4eb806b648b082a6cba7 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 11:10:52 +0300 Subject: [PATCH 09/19] fix --- catalog/app/components/Dialog/Prompt.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index 982c9c2dd9e..958a9d011c0 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -41,12 +41,12 @@ interface PromptProps { } export function usePrompt({ title, onSubmit }: PromptProps) { const [opened, setOpened] = React.useState(false) + const open = React.useCallback(() => setOpened(true), []) + const close = React.useCallback(() => setOpened(false), []) const render = React.useCallback( () => , [close, opened, onSubmit, title], ) - const open = React.useCallback(() => setOpened(true), []) - const close = React.useCallback(() => setOpened(false), []) return React.useMemo( () => ({ close, From a459570afedb3eb636fc16c0b1e2bd2b19a97c59 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 18:09:26 +0300 Subject: [PATCH 10/19] use custom Prompt Dialog --- catalog/app/components/Dialog/index.ts | 1 + catalog/app/containers/Bucket/Dir.tsx | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 catalog/app/components/Dialog/index.ts diff --git a/catalog/app/components/Dialog/index.ts b/catalog/app/components/Dialog/index.ts new file mode 100644 index 00000000000..ff8f16cb3fe --- /dev/null +++ b/catalog/app/components/Dialog/index.ts @@ -0,0 +1 @@ +export * from './Prompt' diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index eef48e204da..3625d8b6464 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -8,6 +8,7 @@ import * as M from '@material-ui/core' import { Crumb, copyWithoutSpaces, render as renderCrumbs } from 'components/BreadCrumbs' import type * as DG from 'components/DataGrid' +import * as Dialog from 'components/Dialog' import * as Bookmarks from 'containers/Bookmarks' import AsyncResult from 'utils/AsyncResult' import * as AWS from 'utils/AWS' @@ -342,26 +343,31 @@ export default function Dir({ [packageDirectoryDialog, path], ) - const createFile = React.useCallback(() => { - const name = window.prompt('Enter file name') - // TODO: if name endsWith unsupported ext - if (!name) return - history.push(urls.bucketFile(bucket, join(path, name), { edit: true })) - }, [bucket, history, path, urls]) + const createFile = React.useCallback( + (name: string) => { + // TODO: if name endsWith unsupported ext + if (!name) return + history.push(urls.bucketFile(bucket, join(path, name), { edit: true })) + }, + [bucket, history, path, urls], + ) + const prompt = Dialog.usePrompt({ title: 'Enter file name', onSubmit: createFile }) const menuItems = React.useMemo( () => [ { - onClick: createFile, + onClick: prompt.open, title: 'Create file', }, ], - [createFile], + [prompt.open], ) return ( {[path || 'Files', bucket]} + {prompt.render()} + {packageDirectoryDialog.render({ successTitle: 'Package created', successRenderMessage: ({ packageLink }) => ( From 5a2d8bc831a6dac643101624f89aaf9950b88f3e Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 19:20:05 +0300 Subject: [PATCH 11/19] validate filename --- catalog/app/components/Dialog/Prompt.tsx | 23 +++++--- catalog/app/containers/Bucket/Dir.tsx | 70 ++++++++++++++++-------- 2 files changed, 62 insertions(+), 31 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index 958a9d011c0..22eabe3b4b9 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -1,33 +1,39 @@ import * as React from 'react' import * as M from '@material-ui/core' +import * as Lab from '@material-ui/lab' interface DialogProps { onCancel: () => void onSubmit: (value: string) => void open: boolean title: string + validate: (value: string) => Error | undefined } // TODO: default value -function Dialog({ open, onCancel, onSubmit, title }: DialogProps) { - const [value, setValue] = React.useState('') +function Dialog({ open, onCancel, onSubmit, title, validate }: DialogProps) { + const [value, setValue] = React.useState('Readme.md') + const error = React.useMemo(() => validate(value), [validate, value]) + const handleChange = React.useCallback((event) => setValue(event.target.value), []) + const handleSubmit = React.useCallback(() => onSubmit(value), [onSubmit, value]) return ( - + {title} setValue(e.target.value)} + onChange={handleChange} value={value} /> + {!!error && {error.message}} Cancel - onSubmit(value)} color="primary" variant="contained"> + Submit @@ -38,14 +44,15 @@ function Dialog({ open, onCancel, onSubmit, title }: DialogProps) { interface PromptProps { title: string onSubmit: (value: string) => void + validate: (value: string) => Error | undefined } -export function usePrompt({ title, onSubmit }: PromptProps) { +export function usePrompt({ title, onSubmit, validate }: PromptProps) { const [opened, setOpened] = React.useState(false) const open = React.useCallback(() => setOpened(true), []) const close = React.useCallback(() => setOpened(false), []) const render = React.useCallback( - () => , - [close, opened, onSubmit, title], + () => , + [close, opened, onSubmit, title, validate], ) return React.useMemo( () => ({ diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index 3625d8b6464..b210a66dbf8 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -9,6 +9,7 @@ import * as M from '@material-ui/core' import { Crumb, copyWithoutSpaces, render as renderCrumbs } from 'components/BreadCrumbs' import type * as DG from 'components/DataGrid' import * as Dialog from 'components/Dialog' +import { detect } from 'components/FileEditor/loader' import * as Bookmarks from 'containers/Bookmarks' import AsyncResult from 'utils/AsyncResult' import * as AWS from 'utils/AWS' @@ -31,6 +32,51 @@ import Summary from './Summary' import { displayError } from './errors' import * as requests from './requests' +interface DirectoryMenuProps { + bucket: string + className?: string + path: string +} + +function DirectoryMenu({ bucket, path, className }: DirectoryMenuProps) { + const { urls } = NamedRoutes.use() + const history = RRDom.useHistory() + + const createFile = React.useCallback( + (name: string) => { + if (!name) return + history.push(urls.bucketFile(bucket, join(path, name), { edit: true })) + }, + [bucket, history, path, urls], + ) + const validateFileName = React.useCallback((value: string) => { + if (!detect(value).brace) { + return new Error('Suppored file formats are JSON, Markdown, YAML and text') + } + }, []) + const prompt = Dialog.usePrompt({ + onSubmit: createFile, + title: 'Enter file name', + validate: validateFileName, + }) + const menuItems = React.useMemo( + () => [ + { + onClick: prompt.open, + title: 'Create file', + }, + ], + [prompt.open], + ) + + return ( + <> + {prompt.render()} + + + ) +} + const useAddToBookmarksStyles = M.makeStyles((t) => ({ root: { alignItems: 'baseline', @@ -266,7 +312,6 @@ export default function Dir({ const { desktop, noDownload } = Config.use() const s3 = AWS.S3.use() const preferences = BucketPreferences.use() - const history = RRDom.useHistory() const { prefix } = parseSearch(l.search) const path = s3paths.decode(encodedPath) const dest = path ? basename(path) : bucket @@ -343,31 +388,10 @@ export default function Dir({ [packageDirectoryDialog, path], ) - const createFile = React.useCallback( - (name: string) => { - // TODO: if name endsWith unsupported ext - if (!name) return - history.push(urls.bucketFile(bucket, join(path, name), { edit: true })) - }, - [bucket, history, path, urls], - ) - const prompt = Dialog.usePrompt({ title: 'Enter file name', onSubmit: createFile }) - const menuItems = React.useMemo( - () => [ - { - onClick: prompt.open, - title: 'Create file', - }, - ], - [prompt.open], - ) - return ( {[path || 'Files', bucket]} - {prompt.render()} - {packageDirectoryDialog.render({ successTitle: 'Package created', successRenderMessage: ({ packageLink }) => ( @@ -397,7 +421,7 @@ export default function Dir({ label="Download directory" /> )} - + {preferences?.ui?.blocks?.code && {code}} From 711a17774a9e89751e3ee8582ae26e74b8054453 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 19:40:51 +0300 Subject: [PATCH 12/19] show error on submit only --- catalog/app/components/Dialog/Prompt.tsx | 8 ++++++-- catalog/app/containers/Bucket/Dir.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index 22eabe3b4b9..ea13809b960 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -13,9 +13,13 @@ interface DialogProps { // TODO: default value function Dialog({ open, onCancel, onSubmit, title, validate }: DialogProps) { const [value, setValue] = React.useState('Readme.md') + const [submited, setSubmited] = React.useState(false) const error = React.useMemo(() => validate(value), [validate, value]) const handleChange = React.useCallback((event) => setValue(event.target.value), []) - const handleSubmit = React.useCallback(() => onSubmit(value), [onSubmit, value]) + const handleSubmit = React.useCallback(() => { + setSubmited(true) + if (!error) onSubmit(value) + }, [error, onSubmit, value]) return ( {title} @@ -27,7 +31,7 @@ function Dialog({ open, onCancel, onSubmit, title, validate }: DialogProps) { onChange={handleChange} value={value} /> - {!!error && {error.message}} + {!!error && !!submited && {error.message}} diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index b210a66dbf8..23cb8a0f41a 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -1,4 +1,4 @@ -import { basename, join } from 'path' +import { basename, join, extname } from 'path' import dedent from 'dedent' import * as R from 'ramda' @@ -50,7 +50,10 @@ function DirectoryMenu({ bucket, path, className }: DirectoryMenuProps) { [bucket, history, path, urls], ) const validateFileName = React.useCallback((value: string) => { - if (!detect(value).brace) { + if (!value) { + return new Error('File name is required') + } + if (!detect(value).brace || extname(value) === '.' || !extname(value)) { return new Error('Suppored file formats are JSON, Markdown, YAML and text') } }, []) From cd509eccdd2474cc3b261d181e9b5b5a82c20ed4 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 19:54:01 +0300 Subject: [PATCH 13/19] clear state on close --- catalog/app/components/Dialog/Prompt.tsx | 54 ++++++++++++++++++++---- catalog/app/containers/Bucket/Dir.tsx | 2 + 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index ea13809b960..e00e92be0cc 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -1,8 +1,10 @@ +import * as R from 'ramda' import * as React from 'react' import * as M from '@material-ui/core' import * as Lab from '@material-ui/lab' interface DialogProps { + initialValue?: string onCancel: () => void onSubmit: (value: string) => void open: boolean @@ -10,9 +12,15 @@ interface DialogProps { validate: (value: string) => Error | undefined } -// TODO: default value -function Dialog({ open, onCancel, onSubmit, title, validate }: DialogProps) { - const [value, setValue] = React.useState('Readme.md') +function Dialog({ + initialValue, + open, + onCancel, + onSubmit, + title, + validate, +}: DialogProps) { + const [value, setValue] = React.useState(initialValue || '') const [submited, setSubmited] = React.useState(false) const error = React.useMemo(() => validate(value), [validate, value]) const handleChange = React.useCallback((event) => setValue(event.target.value), []) @@ -37,7 +45,12 @@ function Dialog({ open, onCancel, onSubmit, title, validate }: DialogProps) { Cancel - + Submit @@ -46,16 +59,41 @@ function Dialog({ open, onCancel, onSubmit, title, validate }: DialogProps) { } interface PromptProps { - title: string + initialValue?: string onSubmit: (value: string) => void + title: string validate: (value: string) => Error | undefined } -export function usePrompt({ title, onSubmit, validate }: PromptProps) { + +export function usePrompt({ initialValue, title, onSubmit, validate }: PromptProps) { + const [key, setKey] = React.useState(0) const [opened, setOpened] = React.useState(false) const open = React.useCallback(() => setOpened(true), []) - const close = React.useCallback(() => setOpened(false), []) + const close = React.useCallback(() => { + setOpened(false) + setKey(R.inc) + }, []) + const handleSubmit = React.useCallback( + (value: string) => { + onSubmit(value) + close() + }, + [close, onSubmit], + ) const render = React.useCallback( - () => , + () => ( + + ), [close, opened, onSubmit, title, validate], ) return React.useMemo( diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index 23cb8a0f41a..4021b912453 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -54,11 +54,13 @@ function DirectoryMenu({ bucket, path, className }: DirectoryMenuProps) { return new Error('File name is required') } if (!detect(value).brace || extname(value) === '.' || !extname(value)) { + // TODO: get list of supported extensions from FileEditor return new Error('Suppored file formats are JSON, Markdown, YAML and text') } }, []) const prompt = Dialog.usePrompt({ onSubmit: createFile, + initialValue: 'README.md', title: 'Enter file name', validate: validateFileName, }) From 0557de88f06d3fb031e634982f7dd51ea81f8d14 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 20:00:31 +0300 Subject: [PATCH 14/19] linting --- catalog/app/components/Dialog/Prompt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index e00e92be0cc..6de4d07323c 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -94,7 +94,7 @@ export function usePrompt({ initialValue, title, onSubmit, validate }: PromptPro }} /> ), - [close, opened, onSubmit, title, validate], + [initialValue, key, close, handleSubmit, opened, title, validate], ) return React.useMemo( () => ({ From c87c2379581bbd0e355ca64704c03af793323ba9 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 20:05:10 +0300 Subject: [PATCH 15/19] handle enter --- catalog/app/components/Dialog/Prompt.tsx | 52 +++++++++++++----------- catalog/app/containers/Bucket/Dir.tsx | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index 6de4d07323c..e9c448f3039 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -30,30 +30,34 @@ function Dialog({ }, [error, onSubmit, value]) return ( - {title} - - - {!!error && !!submited && {error.message}} - - - - Cancel - - - Submit - - +
+ {title} + + + {!!error && !!submited && ( + {error.message} + )} + + + + Cancel + + + Submit + + +
) } diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index 4021b912453..04f8154f03e 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -55,7 +55,7 @@ function DirectoryMenu({ bucket, path, className }: DirectoryMenuProps) { } if (!detect(value).brace || extname(value) === '.' || !extname(value)) { // TODO: get list of supported extensions from FileEditor - return new Error('Suppored file formats are JSON, Markdown, YAML and text') + return new Error('Supported file formats are JSON, Markdown, YAML and text') } }, []) const prompt = Dialog.usePrompt({ From 02d6a2f8217e03a98bc4cfd60453dc1a0db5ed2b Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 20:06:30 +0300 Subject: [PATCH 16/19] hande enter --- catalog/app/components/Dialog/Prompt.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index e9c448f3039..490e4e078f7 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -24,10 +24,14 @@ function Dialog({ const [submited, setSubmited] = React.useState(false) const error = React.useMemo(() => validate(value), [validate, value]) const handleChange = React.useCallback((event) => setValue(event.target.value), []) - const handleSubmit = React.useCallback(() => { - setSubmited(true) - if (!error) onSubmit(value) - }, [error, onSubmit, value]) + const handleSubmit = React.useCallback( + (event) => { + event.preventDefault() + setSubmited(true) + if (!error) onSubmit(value) + }, + [error, onSubmit, value], + ) return (
From ff60a92245c3979be74925b3817f44dcbb864101 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Thu, 25 Aug 2022 22:27:12 +0300 Subject: [PATCH 17/19] add changelog entry --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 59e81790d83..2c50c0985c7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -25,6 +25,7 @@ * [Added] Add missing README to package ([#2960](https://github.com/quiltdata/quilt/pull/2960), [#2979](https://github.com/quiltdata/quilt/pull/2979)) * [Added] View and copy full Athena query by expanding table row ([2993](https://github.com/quiltdata/quilt/pull/2993)) * [Added] Create packages from Athena query results ([#3004](https://github.com/quiltdata/quilt/pull/3004)) +* [Added] Add "Create text file" menu ([#3017](https://github.com/quiltdata/quilt/pull/3017)) * [Fixed] Fix package creation in S3 buckets with SSE-KMS enabled ([#2754](https://github.com/quiltdata/quilt/pull/2754)) * [Fixed] Fix creation of packages with large (4+ GiB) files ([#2933](https://github.com/quiltdata/quilt/pull/2933)) * [Changed] Clean up home page ([#2780](https://github.com/quiltdata/quilt/pull/2780)). From 8022e5fbedabfa7bc4daab93a9cdb0aa0abadb03 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Fri, 26 Aug 2022 16:38:02 +0300 Subject: [PATCH 18/19] fix mistyping --- catalog/app/components/Dialog/Prompt.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index 490e4e078f7..d6eb9a95ea9 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -21,13 +21,13 @@ function Dialog({ validate, }: DialogProps) { const [value, setValue] = React.useState(initialValue || '') - const [submited, setSubmited] = React.useState(false) + const [submitted, setSubmitted] = React.useState(false) const error = React.useMemo(() => validate(value), [validate, value]) const handleChange = React.useCallback((event) => setValue(event.target.value), []) const handleSubmit = React.useCallback( (event) => { event.preventDefault() - setSubmited(true) + setSubmitted(true) if (!error) onSubmit(value) }, [error, onSubmit, value], @@ -44,7 +44,7 @@ function Dialog({ onChange={handleChange} value={value} /> - {!!error && !!submited && ( + {!!error && !!submitted && ( {error.message} )} @@ -54,7 +54,7 @@ function Dialog({ From 17782467bf71d26ed36d60f598ccb4832ae4c792 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Fri, 26 Aug 2022 16:44:58 +0300 Subject: [PATCH 19/19] clean state before opening rather then before closing --- catalog/app/components/Dialog/Prompt.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index d6eb9a95ea9..999d331fa6d 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -76,11 +76,11 @@ interface PromptProps { export function usePrompt({ initialValue, title, onSubmit, validate }: PromptProps) { const [key, setKey] = React.useState(0) const [opened, setOpened] = React.useState(false) - const open = React.useCallback(() => setOpened(true), []) - const close = React.useCallback(() => { - setOpened(false) + const open = React.useCallback(() => { setKey(R.inc) + setOpened(true) }, []) + const close = React.useCallback(() => setOpened(false), []) const handleSubmit = React.useCallback( (value: string) => { onSubmit(value)