Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create text file #3017

Merged
merged 23 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions catalog/app/components/Dialog/Prompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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
title: string
validate: (value: string) => Error | undefined
}

function Dialog({
initialValue,
open,
onCancel,
onSubmit,
title,
validate,
}: DialogProps) {
const [value, setValue] = React.useState(initialValue || '')
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()
setSubmitted(true)
if (!error) onSubmit(value)
},
[error, onSubmit, value],
)
return (
<M.Dialog open={open} fullWidth maxWidth="sm">
<form onSubmit={handleSubmit}>
<M.DialogTitle>{title}</M.DialogTitle>
<M.DialogContent>
<M.TextField
autoFocus
fullWidth
margin="dense"
onChange={handleChange}
value={value}
/>
{!!error && !!submitted && (
<Lab.Alert severity="error">{error.message}</Lab.Alert>
)}
</M.DialogContent>
<M.DialogActions>
<M.Button onClick={onCancel} color="primary" variant="outlined">
Cancel
</M.Button>
<M.Button
color="primary"
disabled={!!error && !!submitted}
onClick={handleSubmit}
variant="contained"
>
Submit
</M.Button>
</M.DialogActions>
</form>
</M.Dialog>
)
}

interface PromptProps {
initialValue?: string
onSubmit: (value: string) => void
title: string
validate: (value: string) => Error | undefined
}

export function usePrompt({ initialValue, title, onSubmit, validate }: PromptProps) {
const [key, setKey] = React.useState(0)
const [opened, setOpened] = React.useState(false)
const open = React.useCallback(() => {
setKey(R.inc)
fiskus marked this conversation as resolved.
Show resolved Hide resolved
setOpened(true)
}, [])
const close = React.useCallback(() => setOpened(false), [])
const handleSubmit = React.useCallback(
(value: string) => {
onSubmit(value)
close()
},
[close, onSubmit],
)
const render = React.useCallback(
() => (
<Dialog
{...{
initialValue,
key,
onCancel: close,
onSubmit: handleSubmit,
open: opened,
title,
validate,
}}
/>
),
[initialValue, key, close, handleSubmit, opened, title, validate],
)
return React.useMemo(
() => ({
close,
open,
render,
}),
[close, open, render],
)
}
1 change: 1 addition & 0 deletions catalog/app/components/Dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Prompt'
68 changes: 66 additions & 2 deletions catalog/app/containers/Bucket/Dir.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { basename, join } from 'path'
import { basename, join, extname } from 'path'

import dedent from 'dedent'
import * as R from 'ramda'
Expand All @@ -8,6 +8,8 @@ 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'
Expand All @@ -23,12 +25,63 @@ 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'

interface DirectoryMenuProps {
bucket: string
className?: string
path: string
}

function DirectoryMenu({ bucket, path, className }: DirectoryMenuProps) {
const { urls } = NamedRoutes.use<RouteMap>()
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 (!value) {
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('Supported file formats are JSON, Markdown, YAML and text')
}
}, [])
const prompt = Dialog.usePrompt({
onSubmit: createFile,
initialValue: 'README.md',
title: 'Enter file name',
validate: validateFileName,
})
const menuItems = React.useMemo(
() => [
{
onClick: prompt.open,
title: 'Create file',
},
],
[prompt.open],
)

return (
<>
{prompt.render()}
<Menu className={className} items={menuItems} />
</>
)
}

const useAddToBookmarksStyles = M.makeStyles((t) => ({
root: {
alignItems: 'baseline',
Expand Down Expand Up @@ -105,7 +158,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<RouteMap>
Expand Down Expand Up @@ -363,6 +426,7 @@ export default function Dir({
label="Download directory"
/>
)}
<DirectoryMenu className={classes.button} bucket={bucket} path={path} />
</M.Box>

{preferences?.ui?.blocks?.code && <Code gutterBottom>{code}</Code>}
Expand Down
6 changes: 3 additions & 3 deletions catalog/app/containers/Bucket/FileView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'))

Expand Down Expand Up @@ -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 (
Expand Down
46 changes: 46 additions & 0 deletions catalog/app/containers/Bucket/Menu.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement | null>(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 (
<>
<M.IconButton className={className} onClick={handleOpen} size="small">
<M.Icon>more_vert</M.Icon>
</M.IconButton>

<M.Menu anchorEl={anchorEl} open={!!anchorEl} onClose={handleClose}>
{items.map(({ onClick, title }) => (
<M.MenuItem key={title} onClick={mkClickHandler(onClick)}>
{title}
</M.MenuItem>
))}
</M.Menu>
</>
)
}
15 changes: 5 additions & 10 deletions catalog/app/containers/Bucket/PackageTree/PackageTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -431,13 +428,11 @@ function DirDisplay({
onClick={openInDesktopState.confirm}
path={downloadPath}
/>
{hasRevisionMenu && (
<RevisionMenu
className={classes.button}
onDelete={confirmDelete}
onDesktop={openInDesktopState.confirm}
/>
)}
<RevisionMenu
className={classes.button}
onDelete={confirmDelete}
onDesktop={openInDesktopState.confirm}
/>
</TopBar>
{preferences?.ui?.blocks?.code && (
<PkgCode {...{ ...packageHandle, hashOrTag, path }} />
Expand Down
Loading