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.
* Convert Boolean to (1|0) and concatenate adresses.
* Parse a front-end value to its API equivalent. This function returns a Promise or an
* 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
* @return {*}
*/
export function formatFormDataValue (value) {
if (typeof value === 'boolean') {
return value ? 1 : 0
} else if (isObjectLiteral(value) && 'separator' in value) {
return Object.values(value).join('')
export function formatFormDataValue (value, key = null) {
if (Array.isArray(value)) {
return Promise.all(
value.map(value_ => formatFormDataValue(value_))
).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 (
formData,
{ extract = null, flatten = false, removeEmpty = true, removeNull = false, multipart = true } = {}
{ extract = null, flatten = false, removeEmpty = true, removeNull = false } = {}
) {
const output = {
data: {},
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)) {
continue
} else if (removeNull && (value === null || value === undefined)) {
} else if (removeNull && [null, undefined].includes(value)) {
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)) {
flattenObjectLiteral(value, output[type])
} else {
output[type][key] = value
}
}
if (promises.length) await Promise.all(promises)
const { data, extracted } = output
return extract ? { data, ...extracted } : data
}

View file

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

View file

@ -124,7 +124,7 @@ export default {
const { data: args, label } = await formatFormData(
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 }

View file

@ -41,23 +41,23 @@ export default {
this.config = formatYunoHostConfigPanels(config)
},
async applyConfig (id_) {
const formatedData = await formatFormData(
this.config.forms[id_],
{ removeEmpty: false, removeNull: true, multipart: false }
async applyConfig (id) {
const args = await formatFormData(
this.config.forms[id],
{ removeEmpty: false, removeNull: true }
)
api.put(
`domains/${this.name}/config`,
{ key: id_, args: objectToParams(formatedData) },
{ key: id, args: objectToParams(args) },
{ key: 'domains.update_config', name: this.name }
).then(() => {
this.$refs.view.fetchQueries({ triggerLoading: true })
}).catch(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) {
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)
})
}