mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-21 01:15:35 +00:00
Dashboard: add required option to metaFields (#2896)
* Dashboard: add `required` option to `metaFields` Fixes: https://github.com/transloadit/uppy/issues/2892 * use `<form>` element to allow users to use all built-in form features from the web browser. * remove `saveOnEnter` hack The fields are now inside a form, saving on Enter is the default behavior – and it doesn't short-circuit form validation checks. * restrictions.requiredMetaFields * fix documentation of restrictions.requiredMetaFields * check for required meta fields on upload * Use detached `<form>` element to avoid nesting `<form>` elements * fixup! Use detached `<form>` element to avoid nesting `<form>` elements * Use `cuid` to generate `<form>` `id`
This commit is contained in:
parent
8ac63d43e0
commit
56b84dfec7
10 changed files with 94 additions and 23 deletions
|
|
@ -32,6 +32,7 @@ const uppy = new Uppy({
|
|||
showProgressDetails: true,
|
||||
proudlyDisplayPoweredByUppy: true,
|
||||
note: '2 files, images and video only',
|
||||
restrictions: { requiredMetaFields: ['caption'] },
|
||||
})
|
||||
.use(GoogleDrive, { target: Dashboard, companionUrl: 'http://localhost:3020' })
|
||||
.use(Instagram, { target: Dashboard, companionUrl: 'http://localhost:3020' })
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ module.exports = () => {
|
|||
username: 'John',
|
||||
license: 'Creative Commons',
|
||||
},
|
||||
restrictions: { requiredMetaFields: ['caption'] },
|
||||
})
|
||||
.use(Dashboard, {
|
||||
trigger: '#pick-files',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,21 @@ class RestrictionError extends Error {
|
|||
this.isRestriction = true
|
||||
}
|
||||
}
|
||||
if (typeof window.AggregateError === 'undefined') {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
AggregateError = class AggregateError extends Error {
|
||||
constructor (message, errors) {
|
||||
super(message)
|
||||
this.errors = errors
|
||||
}
|
||||
}
|
||||
}
|
||||
class AggregateRestrictionError extends AggregateError {
|
||||
constructor (...args) {
|
||||
super(...args)
|
||||
this.isRestriction = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uppy Core module.
|
||||
|
|
@ -51,6 +66,8 @@ class Uppy {
|
|||
0: 'You have to select at least %{smart_count} file',
|
||||
1: 'You have to select at least %{smart_count} files',
|
||||
},
|
||||
missingRequiredMetaField: 'Missing required meta fields',
|
||||
missingRequiredMetaFieldOnFile: 'Missing required meta fields in %{fileName}',
|
||||
// The default `exceedsSize2` string only combines the `exceedsSize` string (%{backwardsCompat}) with the size.
|
||||
// Locales can override `exceedsSize2` to specify a different word order. This is for backwards compat with
|
||||
// Uppy 1.9.x and below which did a naive concatenation of `exceedsSize2 + size` instead of using a locale-specific
|
||||
|
|
@ -108,6 +125,7 @@ class Uppy {
|
|||
maxNumberOfFiles: null,
|
||||
minNumberOfFiles: null,
|
||||
allowedFileTypes: null,
|
||||
requiredMetaFields: [],
|
||||
},
|
||||
meta: {},
|
||||
onBeforeFileAdded: (currentFile) => currentFile,
|
||||
|
|
@ -542,6 +560,33 @@ class Uppy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if requiredMetaField restriction is met before uploading.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
checkRequiredMetaFields (files) {
|
||||
const { requiredMetaFields } = this.opts.restrictions
|
||||
const { hasOwnProperty } = Object.prototype.hasOwnProperty
|
||||
|
||||
const errors = []
|
||||
const fileIDs = Object.keys(files)
|
||||
for (let i = 0; i < fileIDs.length; i++) {
|
||||
const file = this.getFile(fileIDs[i])
|
||||
for (let i = 0; i < requiredMetaFields.length; i++) {
|
||||
if (!hasOwnProperty.call(file.meta, requiredMetaFields[i])) {
|
||||
const err = new RestrictionError(`${this.i18n('missingRequiredMetaFieldOnFile', { fileName: file.name })}`)
|
||||
errors.push(err)
|
||||
this.showOrLogErrorAndThrow(err, { file, throwErr: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
throw new AggregateRestrictionError(`${this.i18n('missingRequiredMetaField')}`, errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error, sets Informer message, then throws the error.
|
||||
* Emits a 'restriction-failed' event if it’s a restriction error
|
||||
|
|
@ -1688,7 +1733,10 @@ class Uppy {
|
|||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => this.checkMinNumberOfFiles(files))
|
||||
.then(() => {
|
||||
this.checkMinNumberOfFiles(files)
|
||||
this.checkRequiredMetaFields(files)
|
||||
})
|
||||
.catch((err) => {
|
||||
this.showOrLogErrorAndThrow(err)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { h, Component } = require('preact')
|
||||
const classNames = require('classnames')
|
||||
const cuid = require('cuid')
|
||||
const getFileTypeIcon = require('../../utils/getFileTypeIcon')
|
||||
const ignoreEvent = require('../../utils/ignoreEvent.js')
|
||||
const FilePreview = require('../FilePreview')
|
||||
|
|
@ -19,15 +20,8 @@ class FileCard extends Component {
|
|||
this.state = {
|
||||
formState: storedMetaData,
|
||||
}
|
||||
}
|
||||
|
||||
saveOnEnter = (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
const file = this.props.files[this.props.fileCardFor]
|
||||
this.props.saveFileCard(this.state.formState, file.id)
|
||||
}
|
||||
this.form.id = cuid()
|
||||
}
|
||||
|
||||
updateMeta = (newVal, name) => {
|
||||
|
|
@ -39,7 +33,10 @@ class FileCard extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleSave = () => {
|
||||
form = document.createElement('form');
|
||||
|
||||
handleSave = (e) => {
|
||||
e.preventDefault()
|
||||
const fileID = this.props.fileCardFor
|
||||
this.props.saveFileCard(this.state.formState, fileID)
|
||||
}
|
||||
|
|
@ -48,6 +45,17 @@ class FileCard extends Component {
|
|||
this.props.toggleFileCard(false)
|
||||
}
|
||||
|
||||
// TODO(aduh95): move this to `UNSAFE_componentWillMount` when updating to Preact X+.
|
||||
componentWillMount () {
|
||||
this.form.addEventListener('submit', this.handleSave)
|
||||
document.body.appendChild(this.form)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.form.removeEventListener('submit', this.handleSave)
|
||||
document.body.removeChild(this.form)
|
||||
}
|
||||
|
||||
renderMetaFields = () => {
|
||||
const metaFields = this.getMetaFields() || []
|
||||
const fieldCSSClasses = {
|
||||
|
|
@ -56,6 +64,7 @@ class FileCard extends Component {
|
|||
|
||||
return metaFields.map((field) => {
|
||||
const id = `uppy-Dashboard-FileCard-input-${field.id}`
|
||||
const required = this.props.requiredMetaFields.includes(field.id)
|
||||
return (
|
||||
<fieldset key={field.id} className="uppy-Dashboard-FileCard-fieldset">
|
||||
<label className="uppy-Dashboard-FileCard-label" htmlFor={id}>{field.name}</label>
|
||||
|
|
@ -64,17 +73,18 @@ class FileCard extends Component {
|
|||
value: this.state.formState[field.id],
|
||||
onChange: (newVal) => this.updateMeta(newVal, field.id),
|
||||
fieldCSSClasses,
|
||||
required,
|
||||
form: this.form.id,
|
||||
}, h)
|
||||
: (
|
||||
<input
|
||||
className={fieldCSSClasses.text}
|
||||
id={id}
|
||||
form={this.form.id}
|
||||
type={field.type || 'text'}
|
||||
required={required}
|
||||
value={this.state.formState[field.id]}
|
||||
placeholder={field.placeholder}
|
||||
onKeyUp={this.saveOnEnter}
|
||||
onKeyDown={this.saveOnEnter}
|
||||
onKeyPress={this.saveOnEnter}
|
||||
onInput={ev => this.updateMeta(ev.target.value, field.id)}
|
||||
data-uppy-super-focusable
|
||||
/>
|
||||
|
|
@ -112,6 +122,7 @@ class FileCard extends Component {
|
|||
<button
|
||||
className="uppy-DashboardContent-back"
|
||||
type="button"
|
||||
form={this.form.id}
|
||||
title={this.props.i18n('finishEditingFile')}
|
||||
onClick={this.handleCancel}
|
||||
>
|
||||
|
|
@ -128,6 +139,7 @@ class FileCard extends Component {
|
|||
type="button"
|
||||
className="uppy-u-reset uppy-c-btn uppy-Dashboard-FileCard-edit"
|
||||
onClick={() => this.props.openFileEditor(file)}
|
||||
form={this.form.id}
|
||||
>
|
||||
{this.props.i18n('editFile')}
|
||||
</button>
|
||||
|
|
@ -141,8 +153,8 @@ class FileCard extends Component {
|
|||
<div className="uppy-Dashboard-FileCard-actions">
|
||||
<button
|
||||
className="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Dashboard-FileCard-actionsBtn"
|
||||
type="button"
|
||||
onClick={this.handleSave}
|
||||
type="submit"
|
||||
form={this.form.id}
|
||||
>
|
||||
{this.props.i18n('saveChanges')}
|
||||
</button>
|
||||
|
|
@ -150,6 +162,7 @@ class FileCard extends Component {
|
|||
className="uppy-u-reset uppy-c-btn uppy-c-btn-link uppy-Dashboard-FileCard-actionsBtn"
|
||||
type="button"
|
||||
onClick={this.handleCancel}
|
||||
form={this.form.id}
|
||||
>
|
||||
{this.props.i18n('cancel')}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1025,6 +1025,7 @@ module.exports = class Dashboard extends Plugin {
|
|||
parentElement: this.el,
|
||||
allowedFileTypes: this.uppy.opts.restrictions.allowedFileTypes,
|
||||
maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles,
|
||||
requiredMetaFields: this.uppy.opts.restrictions.requiredMetaFields,
|
||||
showSelectedFiles: this.opts.showSelectedFiles,
|
||||
handleCancelRestore: this.handleCancelRestore,
|
||||
handleRequestThumbnail: this.handleRequestThumbnail,
|
||||
|
|
|
|||
2
packages/@uppy/dashboard/types/index.d.ts
vendored
2
packages/@uppy/dashboard/types/index.d.ts
vendored
|
|
@ -5,6 +5,8 @@ import DashboardLocale = require('./generatedLocale')
|
|||
type FieldRenderOptions = {
|
||||
value: string,
|
||||
onChange: (newVal: string) => void
|
||||
fieldCSSClasses: { text: string }
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
type PreactRender = (node: any, params: object | null, ...children: any[]) => any
|
||||
|
|
|
|||
|
|
@ -234,8 +234,8 @@ An array of UI field objects, or a function that takes a [File Object](https://u
|
|||
- `name`, the label shown in the interface.
|
||||
- `placeholder`, the text shown when no value is set in the field. (Not needed when a custom render function is provided)
|
||||
|
||||
Optionally, you can specify `render: ({value, onChange}, h) => void`, a function for rendering a custom form element.
|
||||
It gets passed `({value, onChange}, h)` where `value` is the current value of the meta field, `onChange: (newVal) => void` is a function saving the new value and `h` is the `createElement` function from [preact](https://preactjs.com/guide/v10/api-reference#h--createelement).
|
||||
Optionally, you can specify `render: ({value, onChange, required}, h) => void`, a function for rendering a custom form element.
|
||||
It gets passed `({value, onChange, required}, h)` where `value` is the current value of the meta field, `required` is a boolean that's true if the field `id` is in the `restrictedMetaFields` restriction, and `onChange: (newVal) => void` is a function saving the new value and `h` is the `createElement` function from [preact](https://preactjs.com/guide/v10/api-reference#h--createelement).
|
||||
`h` can be useful when using uppy from plain JavaScript, where you cannot write JSX.
|
||||
|
||||
```js
|
||||
|
|
@ -245,10 +245,10 @@ It gets passed `({value, onChange}, h)` where `value` is the current value of th
|
|||
{ id: 'name', name: 'Name', placeholder: 'file name' },
|
||||
{ id: 'license', name: 'License', placeholder: 'specify license' },
|
||||
{ id: 'caption', name: 'Caption', placeholder: 'describe what the image is about' },
|
||||
{ id: 'public', name: 'Public', render: function({value, onChange}, h) {
|
||||
return h('input', { type: 'checkbox', onChange: (ev) => onChange(ev.target.checked ? 'on' : 'off'), defaultChecked: value === 'on' })
|
||||
{ id: 'public', name: 'Public', render: function({value, onChange, required}, h) {
|
||||
return h('input', { type: 'checkbox', required, onChange: (ev) => onChange(ev.target.checked ? 'on' : 'off'), defaultChecked: value === 'on' })
|
||||
} }
|
||||
]
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
|
|
@ -265,11 +265,12 @@ If you’d like the meta fields to be dynamically assigned depending on, for ins
|
|||
fields.push({
|
||||
id: 'public',
|
||||
name: 'Public',
|
||||
render: ({ value, onChange }, h) => {
|
||||
render: ({ value, onChange, required }, h) => {
|
||||
return h('input', {
|
||||
type: 'checkbox',
|
||||
onChange: (ev) => onChange(ev.target.checked ? 'on' : 'off'),
|
||||
defaultChecked: value === 'on',
|
||||
required,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ const uppy = new Uppy({
|
|||
maxTotalFileSize: null,
|
||||
maxNumberOfFiles: null,
|
||||
minNumberOfFiles: null,
|
||||
allowedFileTypes: null
|
||||
allowedFileTypes: null,
|
||||
requiredMetaFields: [],
|
||||
},
|
||||
meta: {},
|
||||
onBeforeFileAdded: (currentFile, files) => currentFile,
|
||||
|
|
@ -170,6 +171,7 @@ Optionally, provide rules and conditions to limit the type and/or number of file
|
|||
- `maxNumberOfFiles` *null | number* — total number of files that can be selected
|
||||
- `minNumberOfFiles` *null | number* — minimum number of files that must be selected before the upload
|
||||
- `allowedFileTypes` *null | array* of wildcards `image/*`, exact mime types `image/jpeg`, or file extensions `.jpg`: `['image/*', '.jpg', '.jpeg', '.png', '.gif']`
|
||||
- `requiredMetaFields` *array* of strings
|
||||
|
||||
`maxNumberOfFiles` also affects the number of files a user is able to select via the system file dialog in UI plugins like `DragDrop`, `FileInput` and `Dashboard`: when set to `1`, they will only be able to select a single file. When `null` or another number is provided, they will be able to select multiple files.
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ function uppyInit () {
|
|||
|
||||
const uppy = new Uppy({
|
||||
logger: Uppy.debugLogger,
|
||||
restrictions: { requiredMetaFields: ['caption'] }
|
||||
})
|
||||
|
||||
uppy.use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/', resume: true })
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ const uppy = new Uppy({
|
|||
maxFileSize: 1000000,
|
||||
maxNumberOfFiles: 3,
|
||||
minNumberOfFiles: 2,
|
||||
allowedFileTypes: ['image/*', 'video/*']
|
||||
allowedFileTypes: ['image/*', 'video/*'],
|
||||
requiredMetaFields: ['caption'],
|
||||
}
|
||||
})
|
||||
.use(Dashboard, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue