Skip to content

Commit

Permalink
Package name validation (#1998)
Browse files Browse the repository at this point in the history
* Package name error is shown immediately
* Error message moved to helperText (text under Input)
* Warning messages added to package name Input
* Fixed bug with infinite spinner after losing focus
  • Loading branch information
fiskus authored Jan 15, 2021
1 parent 3a06277 commit dd6ad48
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 47 deletions.
40 changes: 30 additions & 10 deletions catalog/app/containers/Bucket/PackageCopyDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ function DialogTitle({ bucket }) {
)
}

const defaultNameWarning = ' ' // Reserve space for warning

const useStyles = M.makeStyles((t) => ({
meta: {
marginTop: t.spacing(3),
Expand All @@ -92,6 +94,8 @@ function DialogForm({
workflowsConfig,
}) {
const nameValidator = PD.useNameValidator()
const nameExistence = PD.useNameExistence(successor.slug)
const [nameWarning, setNameWarning] = React.useState('')
const classes = useStyles()

const initialMeta = React.useMemo(
Expand Down Expand Up @@ -128,6 +132,27 @@ function DialogForm({
}
}

const onFormChange = React.useCallback(
async ({ values }) => {
const { name } = values
const fullName = `${successor.slug}/${name}`

const nameExists = await nameExistence.validate(name)
if (nameExists) {
setNameWarning(`Package "${fullName}" exists. Submitting will revise it`)
return
}

if (name) {
setNameWarning(`Package "${fullName}" will be created`)
return
}

setNameWarning(defaultNameWarning)
},
[successor, nameExistence],
)

return (
<RF.Form onSubmit={onSubmit}>
{({
Expand All @@ -144,11 +169,11 @@ function DialogForm({
<DialogTitle bucket={successor.slug} />
<M.DialogContent style={{ paddingTop: 0 }}>
<form onSubmit={handleSubmit}>
<RF.FormSpy subscription={{ values: true }} onChange={onFormChange} />

<RF.Field
component={PD.Field}
component={PD.PackageNameInput}
name="name"
label="Name"
placeholder="e.g. user/package"
validate={validators.composeAsync(
validators.required,
nameValidator.validate,
Expand All @@ -158,23 +183,18 @@ function DialogForm({
required: 'Enter a package name',
invalid: 'Invalid package name',
}}
margin="normal"
fullWidth
helperText={nameWarning}
initialValue={initialName}
/>

<RF.Field
component={PD.Field}
component={PD.CommitMessageInput}
name="commitMessage"
label="Commit message"
placeholder="Enter a commit message"
validate={validators.required}
validateFields={['commitMessage']}
errors={{
required: 'Enter a commit message',
}}
fullWidth
margin="normal"
/>

<PD.SchemaFetcher
Expand Down
41 changes: 31 additions & 10 deletions catalog/app/containers/Bucket/PackageCreateDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ const getTotalProgress = R.pipe(
}),
)

const defaultNameWarning = ' ' // Reserve space for warning

function PackageCreateDialog({
bucket,
open,
Expand All @@ -356,13 +358,17 @@ function PackageCreateDialog({
const [uploads, setUploads] = React.useState({})
const [success, setSuccess] = React.useState(null)
const nameValidator = PD.useNameValidator()
const nameExistence = PD.useNameExistence(bucket)
const [nameWarning, setNameWarning] = React.useState(defaultNameWarning)
const classes = useStyles()

const reset = (form) => () => {
form.restart()
setSuccess(null)
setUploads({})
nameValidator.inc()
nameExistence.inc()
setNameWarning(defaultNameWarning)
}

const handleClose = ({ submitting = false } = {}) => () => {
Expand Down Expand Up @@ -470,6 +476,22 @@ function PackageCreateDialog({
}
}

const onFormChange = React.useCallback(
async ({ modified, values }) => {
if (!modified.name) return

const { name } = values

setNameWarning(defaultNameWarning)

const nameExists = await nameExistence.validate(name)
if (nameExists) {
setNameWarning(`Package "${name}" exists. Submitting will revise it`)
}
},
[nameExistence],
)

return (
<RF.Form onSubmit={uploadPackage}>
{({
Expand Down Expand Up @@ -541,11 +563,14 @@ function PackageCreateDialog({
<M.DialogTitle>Create package</M.DialogTitle>
<M.DialogContent style={{ paddingTop: 0 }}>
<form onSubmit={handleSubmit}>
<RF.FormSpy
subscription={{ modified: true, values: true }}
onChange={onFormChange}
/>

<RF.Field
component={PD.Field}
component={PD.PackageNameInput}
name="name"
label="Name"
placeholder="e.g. user/package"
validate={validators.composeAsync(
validators.required,
nameValidator.validate,
Expand All @@ -555,22 +580,18 @@ function PackageCreateDialog({
required: 'Enter a package name',
invalid: 'Invalid package name',
}}
margin="normal"
fullWidth
helperText={nameWarning}
validating={nameValidator.processing}
/>

<RF.Field
component={PD.Field}
component={PD.CommitMessageInput}
name="msg"
label="Commit message"
placeholder="Enter a commit message"
validate={validators.required}
validateFields={['msg']}
errors={{
required: 'Enter a commit message',
}}
fullWidth
margin="normal"
/>

<RF.Field
Expand Down
99 changes: 82 additions & 17 deletions catalog/app/containers/Bucket/PackageDialog/PackageDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,25 +92,61 @@ const readFile = (file) =>
reader.readAsText(file)
})

const validateName = (req) =>
cacheDebounce(async (name) => {
if (name) {
const res = await req({
endpoint: '/package_name_valid',
method: 'POST',
body: { name },
})
if (!res.valid) return 'invalid'
}
return undefined
}, 200)

export function useNameValidator() {
const req = APIConnector.use()
const [counter, setCounter] = React.useState(0)
const [processing, setProcessing] = React.useState(false)
const inc = React.useCallback(() => setCounter(R.inc), [setCounter])

const validator = React.useMemo(() => validateName(req), [req])

const validate = React.useCallback(
async (name) => {
setProcessing(true)
const error = await validator(name)
setProcessing(false)
return error
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[counter, validator],
)

return React.useMemo(() => ({ validate, processing, inc }), [validate, processing, inc])
}

export function useNameExistence(bucket) {
const [counter, setCounter] = React.useState(0)
const inc = React.useCallback(() => setCounter(R.inc), [setCounter])

const s3 = AWS.S3.use()

// eslint-disable-next-line react-hooks/exhaustive-deps
const validate = React.useCallback(
cacheDebounce(async (name) => {
if (name) {
const res = await req({
endpoint: '/package_name_valid',
method: 'POST',
body: { name },
const packageExists = await requests.ensurePackageIsPresent({
s3,
bucket,
name,
})
if (!res.valid) return 'invalid'
if (packageExists) return 'exists'
}
return undefined
}, 200),
[req, counter],
[bucket, counter, s3],
)

return React.useMemo(() => ({ validate, inc }), [validate, inc])
Expand Down Expand Up @@ -153,23 +189,52 @@ export const getMetaValue = (value) =>
)
: undefined

export function Field({ input, meta, errors, label, ...rest }) {
const error = meta.submitFailed && meta.error
const validating = meta.submitFailed && meta.validating
export function Field({ error, helperText, validating, warning, ...rest }) {
const props = {
InputLabelProps: { shrink: true },
InputProps: {
endAdornment: validating && <M.CircularProgress size={20} />,
},
error: !!error,
label: (
<>
{error ? errors[error] || error : label}
{validating && <M.CircularProgress size={13} style={{ marginLeft: 8 }} />}
</>
),
helperText: error || helperText,
...rest,
}
return <M.TextField {...props} />
}

export function PackageNameInput({ errors, input, meta, validating, ...rest }) {
const errorCode = (input.value || meta.submitFailed) && meta.error
const error = errorCode ? errors[errorCode] || errorCode : ''
const props = {
disabled: meta.submitting || meta.submitSucceeded,
InputLabelProps: { shrink: true },
error,
fullWidth: true,
label: 'Name',
margin: 'normal',
placeholder: 'e.g. user/package',
// NOTE: react-form doesn't change `FormState.validating` on async validation when field loses focus
validating,
...input,
...rest,
}
return <M.TextField {...props} />
return <Field {...props} />
}

export function CommitMessageInput({ errors, input, meta, ...rest }) {
const errorCode = meta.submitFailed && meta.error
const error = errorCode ? errors[errorCode] || errorCode : ''
const props = {
disabled: meta.submitting || meta.submitSucceeded,
error,
fullWidth: true,
label: 'Commit message',
margin: 'normal',
placeholder: 'Enter a commit message',
validating: meta.submitFailed && meta.validating,
...input,
...rest,
}
return <Field {...props} />
}

const useWorkflowInputStyles = M.makeStyles((t) => ({
Expand Down
Loading

0 comments on commit dd6ad48

Please sign in to comment.