1
0
mirror of synced 2025-03-03 11:33:17 +03:00

Fixes issues with upload field re-hydration and requried validation rule for FileUpload fields.

This commit is contained in:
Justin Schroeder 2020-03-02 00:35:56 -05:00
commit c6eae3493b
14 changed files with 176 additions and 20 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
dist/snow.css vendored
View File

@ -397,7 +397,8 @@
z-index: 2; } z-index: 2; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name { .formulate-input[data-classification="file"] .formulate-files .formualte-file-name {
padding-left: 1.5em; padding-left: 1.5em;
padding-right: 2em; } padding-right: 2em;
max-width: 100%; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name::before { .formulate-input[data-classification="file"] .formulate-files .formualte-file-name::before {
position: absolute; position: absolute;
left: .7em; left: .7em;

2
dist/snow.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import nanoid from 'nanoid/non-secure' import nanoid from 'nanoid/non-secure'
import mimes from './libs/mimes'
/** /**
* The file upload class holds and represents a files upload state durring * The file upload class holds and represents a files upload state durring
@ -15,9 +16,35 @@ class FileUpload {
this.fileList = input.files this.fileList = input.files
this.files = [] this.files = []
this.options = options this.options = options
this.addFileList(this.fileList)
this.context = context
this.results = false this.results = false
this.context = context
if (Array.isArray(this.fileList)) {
this.rehydrateFileList(this.fileList)
} else {
this.addFileList(this.fileList)
}
}
/**
* Given a pre-existing array of files, create a faux FileList.
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
* @param {string} pathKey the object-key to access the url (defaults to "url")
*/
rehydrateFileList (items) {
const fauxFileList = items.reduce((fileList, item) => {
const key = this.options ? this.options.fileUrlKey : 'url'
const url = item[key]
const ext = (url && url.lastIndexOf('.') !== -1) ? url.substr(url.lastIndexOf('.') + 1) : false
const mime = mimes[ext] || false
fileList.push(Object.assign({}, item, url ? {
name: url.substr((url.lastIndexOf('/') + 1) || 0),
type: item.type ? item.type : mime,
previewData: url
} : {}))
return fileList
}, [])
this.results = items
this.addFileList(fauxFileList)
} }
/** /**
@ -41,7 +68,7 @@ class FileUpload {
uuid, uuid,
path: false, path: false,
removeFile: removeFile.bind(this), removeFile: removeFile.bind(this),
previewData: false previewData: file.previewData || false
}) })
} }
} }
@ -54,15 +81,18 @@ class FileUpload {
} }
/** /**
* Check if the given uploader is axios instance. * Check if the given uploader is axios instance. This isn't a great way of
* testing if it is or not, but AFIK there isn't a better way right now:
*
* https://github.com/axios/axios/issues/737
*/ */
uploaderIsAxios () { uploaderIsAxios () {
if ( if (
this.hasUploader && this.hasUploader &&
typeof this.hasUploader.request === 'function' && typeof this.context.uploader.request === 'function' &&
typeof this.hasUploader.get === 'function' && typeof this.context.uploader.get === 'function' &&
typeof this.hasUploader.delete === 'function' && typeof this.context.uploader.delete === 'function' &&
typeof this.hasUploader.post === 'function' typeof this.context.uploader.post === 'function'
) { ) {
return true return true
} }
@ -76,14 +106,19 @@ class FileUpload {
if (this.uploaderIsAxios()) { if (this.uploaderIsAxios()) {
const formData = new FormData() const formData = new FormData()
formData.append(this.context.name || 'file', args[0]) formData.append(this.context.name || 'file', args[0])
return this.uploader.post(this.context.uploadUrl, formData, { if (this.context.uploadUrl === false) {
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props')
}
return this.context.uploader.post(this.context.uploadUrl, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
}, },
onUploadProgress: progressEvent => { onUploadProgress: progressEvent => {
// args[1] here is the upload progress handler function
args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total)) args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total))
} }
}) })
.then(res => res.data)
.catch(err => args[2](err)) .catch(err => args[2](err))
} }
return this.context.uploader(...args) return this.context.uploader(...args)
@ -135,7 +170,8 @@ class FileUpload {
*/ */
removeFile (uuid) { removeFile (uuid) {
this.files = this.files.filter(file => file.uuid !== uuid) this.files = this.files.filter(file => file.uuid !== uuid)
if (window) { this.context.performValidation()
if (window && this.fileList instanceof FileList) {
const transfer = new DataTransfer() const transfer = new DataTransfer()
this.files.map(file => transfer.items.add(file.file)) this.files.map(file => transfer.items.add(file.file))
this.fileList = transfer.files this.fileList = transfer.files

View File

@ -43,6 +43,8 @@ class Formulate {
rules, rules,
locale: 'en', locale: 'en',
uploader: fauxUploader, uploader: fauxUploader,
uploadUrl: false,
fileUrlKey: 'url',
uploadJustCompleteDuration: 1000, uploadJustCompleteDuration: 1000,
plugins: [], plugins: [],
locales: { locales: {
@ -162,6 +164,21 @@ class Formulate {
return this.options.uploader || false return this.options.uploader || false
} }
/**
* Get the global upload url.
*/
getUploadUrl () {
return this.options.uploadUrl || false
}
/**
* When re-hydrating a file uploader with an array, get the sub-object key to
* access the url of the file. Usually this is just "url".
*/
getFileUrlKey () {
return this.options.fileUrlKey || 'url'
}
/** /**
* Create a new instance of an upload. * Create a new instance of an upload.
*/ */

View File

@ -36,6 +36,7 @@ export default {
attributes: { id, ...groupApplicableAttributes }, attributes: { id, ...groupApplicableAttributes },
classification, classification,
blurHandler, blurHandler,
performValidation,
hasValidationErrors, hasValidationErrors,
component, component,
hasLabel, hasLabel,

View File

@ -52,6 +52,15 @@ export default {
return !!(this.context.model instanceof FileUpload && this.context.model.files.length) return !!(this.context.model instanceof FileUpload && this.context.model.files.length)
} }
}, },
created () {
if (Array.isArray(this.context.model)) {
if (typeof this.context.model[0][this.$formulate.getFileUrlKey()] === 'string') {
this.context.model = this.$formulate.createUpload({
files: this.context.model
}, this.context)
}
}
},
mounted () { mounted () {
// Add a listener to the window to prevent drag/drops that miss the dropzone // Add a listener to the window to prevent drag/drops that miss the dropzone
// from opening the file and navigating the user away from the page. // from opening the file and navigating the user away from the page.

View File

@ -19,9 +19,10 @@ export default {
label: this.label, label: this.label,
labelPosition: this.logicalLabelPosition, labelPosition: this.logicalLabelPosition,
attributes: this.elementAttributes, attributes: this.elementAttributes,
performValidation: this.performValidation.bind(this),
blurHandler: blurHandler.bind(this), blurHandler: blurHandler.bind(this),
imageBehavior: this.imageBehavior, imageBehavior: this.imageBehavior,
uploadUrl: this.uploadUrl, uploadUrl: this.mergedUploadUrl,
uploader: this.uploader || this.$formulate.getUploader(), uploader: this.uploader || this.$formulate.getUploader(),
uploadBehavior: this.uploadBehavior, uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops, preventWindowDrops: this.preventWindowDrops,
@ -37,7 +38,8 @@ export default {
mergedErrors, mergedErrors,
hasErrors, hasErrors,
showFieldErrors, showFieldErrors,
mergedValidationName mergedValidationName,
mergedUploadUrl
} }
/** /**
@ -111,6 +113,14 @@ function mergedValidationName () {
return this.type return this.type
} }
/**
* Use the uploadURL on the input if it exists, otherwise use the uploadURL
* that is defined as a plugin option.
*/
function mergedUploadUrl () {
return this.uploadUrl || this.$formulate.getUploadUrl()
}
/** /**
* Determines if the field should show it's error (if it has one) * Determines if the field should show it's error (if it has one)
* @return {boolean} * @return {boolean}

74
src/libs/mimes.js Normal file
View File

@ -0,0 +1,74 @@
export default {
'aac': 'audio/aac',
'abw': 'application/x-abiword',
'arc': 'application/x-freearc',
'avi': 'video/x-msvideo',
'azw': 'application/vnd.amazon.ebook',
'bin': 'application/octet-stream',
'bmp': 'image/bmp',
'bz': 'application/x-bzip',
'bz2': 'application/x-bzip2',
'csh': 'application/x-csh',
'css': 'text/css',
'csv': 'text/csv',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'eot': 'application/vnd.ms-fontobject',
'epub': 'application/epub+zip',
'gz': 'application/gzip',
'gif': 'image/gif',
'htm': 'text/html',
'html': 'text/html',
'ico': 'image/vnd.microsoft.icon',
'ics': 'text/calendar',
'jar': 'application/java-archive',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'js': 'text/javascript',
'json': 'application/json',
'jsonld': 'application/ld+json',
'midi': 'audio/x-midi',
'mid': 'audio/midi',
'mjs': 'text/javascript',
'mp3': 'audio/mpeg',
'mpeg': 'video/mpeg',
'mpkg': 'application/vnd.apple.installer+xml',
'odp': 'application/vnd.oasis.opendocument.presentation',
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
'odt': 'application/vnd.oasis.opendocument.text',
'oga': 'audio/ogg',
'ogv': 'video/ogg',
'ogx': 'application/ogg',
'opus': 'audio/opus',
'otf': 'font/otf',
'png': 'image/png',
'pdf': 'application/pdf',
'php': 'application/php',
'ppt': 'application/vnd.ms-powerpoint',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'rar': 'application/vnd.rar',
'rtf': 'application/rtf',
'sh': 'application/x-sh',
'svg': 'image/svg+xml',
'swf': 'application/x-shockwave-flash',
'tar': 'application/x-tar',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'ts': 'video/mp2t',
'ttf': 'font/ttf',
'txt': 'text/plain',
'vsd': 'application/vnd.visio',
'wav': 'audio/wav',
'weba': 'audio/webm',
'webm': 'video/webm',
'webp': 'image/webp',
'woff': 'font/woff',
'woff2': 'font/woff2',
'xhtml': 'application/xhtml+xml',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xml': 'text/xml',
'xul': 'application/vnd.mozilla.xul+xml',
'zip': 'application/zip',
'7z': 'application/x-7z-compressed'
}

View File

@ -164,9 +164,9 @@ export default {
mime: function ({ value }, ...types) { mime: function ({ value }, ...types) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (value instanceof FileUpload) { if (value instanceof FileUpload) {
const fileList = value.getFileList() const fileList = value.getFiles()
for (let i = 0; i < fileList.length; i++) { for (let i = 0; i < fileList.length; i++) {
const file = fileList[i] const file = fileList[i].file
if (!types.includes(file.type)) { if (!types.includes(file.type)) {
return false return false
} }
@ -227,6 +227,9 @@ export default {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return !!value.length return !!value.length
} }
if (value instanceof FileUpload) {
return value.getFiles().length > 0
}
if (typeof value === 'string') { if (typeof value === 'string') {
return !!value return !!value
} }

View File

@ -24,6 +24,10 @@ describe('required', () => {
it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true)) it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true))
it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true)) it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true))
it('passes with FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [{ name: 'j.png' }] }) })).toBe(true))
it('fails with empty FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [] }) })).toBe(false))
}) })

View File

@ -501,6 +501,7 @@
.formualte-file-name { .formualte-file-name {
padding-left: 1.5em; padding-left: 1.5em;
padding-right: 2em; padding-right: 2em;
max-width: 100%;
&::before { &::before {
position: absolute; position: absolute;