rework form parsing to properly handle async values

This commit is contained in:
axolotle 2022-03-01 16:53:50 +01:00
parent 82aab9a984
commit 76c6a0eb70
4 changed files with 72 additions and 37 deletions

View file

@ -375,19 +375,64 @@ export function formatYunoHostConfigPanels (data) {
/** /**
* Format helper for a form value. * Parse a front-end value to its API equivalent. This function returns a Promise or an
* Convert Boolean to (1|0) and concatenate adresses. * Object `{ key: Promise }` if `key` is supplied. When parsing a form, all those
* objects must be merged to define the final sent form.
*
* Convert Boolean to '1' (true) or '0' (false),
* Concatenate two parts adresses (subdomain or email for example) into a single string,
* Convert File to its Base64 representation or set its value to '' to ask for a removal.
* *
* @param {*} value * @param {*} value
* @return {*} * @return {*}
*/ */
export function formatFormDataValue (value) { export function formatFormDataValue (value, key = null) {
if (typeof value === 'boolean') { if (Array.isArray(value)) {
return value ? 1 : 0 return Promise.all(
} else if (isObjectLiteral(value) && 'separator' in value) { value.map(value_ => formatFormDataValue(value_))
return Object.values(value).join('') ).then(resolvedValues => ({ [key]: resolvedValues }))
} }
return value
let result = value
if (typeof value === 'boolean') result = value ? 1 : 0
if (isObjectLiteral(value) && 'file' in value) {
// File has to be deleted
if (value.removed) result = ''
// File has not changed (will not be sent)
else if (value.current_file || value.file === null) result = null
else {
return getFileContent(value.file, { base64: true }).then(content => {
return {
[key]: content.replace(/data:[^;]*;base64,/, ''),
[key + '[name]']: value.file.name
}
})
}
} else if (isObjectLiteral(value) && 'separator' in value) {
result = Object.values(value).join('')
}
// Returns a resolved Promise for non async values
return Promise.resolve(key ? { [key]: result } : result)
}
/**
* Convinient helper to properly parse a front-end form to its API equivalent.
* This parse each values asynchronously, allow to inject keys into the final form and
* make sure every async values resolves before resolving itself.
*
* @param {Object} formData
* @return {Object}
*/
function formatFormDataValues (formData) {
const promisedValues = Object.entries(formData).map(([key, value]) => {
return formatFormDataValue(value, key)
})
return Promise.all(promisedValues).then(resolvedValues => {
return resolvedValues.reduce((form, obj) => ({ ...form, ...obj }), {})
})
} }
@ -403,38 +448,28 @@ export function formatFormDataValue (value) {
*/ */
export async function formatFormData ( export async function formatFormData (
formData, formData,
{ extract = null, flatten = false, removeEmpty = true, removeNull = false, multipart = true } = {} { extract = null, flatten = false, removeEmpty = true, removeNull = false } = {}
) { ) {
const output = { const output = {
data: {}, data: {},
extracted: {} extracted: {}
} }
const promises = []
for (const key in formData) {
const type = extract && extract.includes(key) ? 'extracted' : 'data'
const value = Array.isArray(formData[key])
? formData[key].map(item => formatFormDataValue(item))
: formatFormDataValue(formData[key])
const values = await formatFormDataValues(formData)
for (const key in values) {
const type = extract && extract.includes(key) ? 'extracted' : 'data'
const value = values[key]
if (removeEmpty && isEmptyValue(value)) { if (removeEmpty && isEmptyValue(value)) {
continue continue
} else if (removeNull && (value === null || value === undefined)) { } else if (removeNull && [null, undefined].includes(value)) {
continue continue
} else if (value instanceof File && !multipart) {
if (value.currentfile) {
continue
} else if (value._removed) {
output[type][key] = ''
continue
}
promises.push(pFileReader(value, output[type], key))
} else if (flatten && isObjectLiteral(value)) { } else if (flatten && isObjectLiteral(value)) {
flattenObjectLiteral(value, output[type]) flattenObjectLiteral(value, output[type])
} else { } else {
output[type][key] = value output[type][key] = value
} }
} }
if (promises.length) await Promise.all(promises)
const { data, extracted } = output const { data, extracted } = output
return extract ? { data, ...extracted } : data return extract ? { data, ...extracted } : data
} }

View file

@ -50,16 +50,16 @@ export default {
}, },
async applyConfig (id_) { async applyConfig (id_) {
const formatedData = await formatFormData( const args = await formatFormData(
this.config.forms[id_], this.config.forms[id_],
{ removeEmpty: false, removeNull: true, multipart: false } { removeEmpty: false, removeNull: true }
) )
api.put( api.put(
`apps/${this.id}/config`, `apps/${this.id}/config`,
{ key: id_, args: objectToParams(formatedData) }, { key: id_, args: objectToParams(args) },
{ key: 'apps.update_config', name: this.id } { key: 'apps.update_config', name: this.id }
).then(response => { ).then(() => {
this.$refs.view.fetchQueries({ triggerLoading: true }) this.$refs.view.fetchQueries({ triggerLoading: true })
}).catch(err => { }).catch(err => {
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err

View file

@ -124,7 +124,7 @@ export default {
const { data: args, label } = await formatFormData( const { data: args, label } = await formatFormData(
this.form, this.form,
{ extract: ['label'], removeEmpty: false, removeNull: true, multipart: false } { extract: ['label'], removeEmpty: false, removeNull: true }
) )
const data = { app: this.id, label, args: Object.entries(args).length ? objectToParams(args) : undefined } const data = { app: this.id, label, args: Object.entries(args).length ? objectToParams(args) : undefined }

View file

@ -41,23 +41,23 @@ export default {
this.config = formatYunoHostConfigPanels(config) this.config = formatYunoHostConfigPanels(config)
}, },
async applyConfig (id_) { async applyConfig (id) {
const formatedData = await formatFormData( const args = await formatFormData(
this.config.forms[id_], this.config.forms[id],
{ removeEmpty: false, removeNull: true, multipart: false } { removeEmpty: false, removeNull: true }
) )
api.put( api.put(
`domains/${this.name}/config`, `domains/${this.name}/config`,
{ key: id_, args: objectToParams(formatedData) }, { key: id, args: objectToParams(args) },
{ key: 'domains.update_config', name: this.name } { key: 'domains.update_config', name: this.name }
).then(() => { ).then(() => {
this.$refs.view.fetchQueries({ triggerLoading: true }) this.$refs.view.fetchQueries({ triggerLoading: true })
}).catch(err => { }).catch(err => {
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err
const panel = this.config.panels.find(({ id }) => id_ === id) const panel = this.config.panels.find(panel => panel.id === id)
if (err.data.name) { if (err.data.name) {
this.config.errors[id_][err.data.name].message = err.message this.config.errors[id][err.data.name].message = err.message
} else this.$set(panel, 'serverError', err.message) } else this.$set(panel, 'serverError', err.message)
}) })
} }