From 76c6a0eb7053c4ba185d25e0e235531e6e449ce9 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 1 Mar 2022 16:53:50 +0100 Subject: [PATCH] rework form parsing to properly handle async values --- app/src/helpers/yunohostArguments.js | 85 +++++++++++++++++++-------- app/src/views/app/AppConfigPanel.vue | 8 +-- app/src/views/app/AppInstall.vue | 2 +- app/src/views/domain/DomainConfig.vue | 14 ++--- 4 files changed, 72 insertions(+), 37 deletions(-) diff --git a/app/src/helpers/yunohostArguments.js b/app/src/helpers/yunohostArguments.js index b747483f..51c1e6e1 100644 --- a/app/src/helpers/yunohostArguments.js +++ b/app/src/helpers/yunohostArguments.js @@ -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 } diff --git a/app/src/views/app/AppConfigPanel.vue b/app/src/views/app/AppConfigPanel.vue index cb619241..dc36aa72 100644 --- a/app/src/views/app/AppConfigPanel.vue +++ b/app/src/views/app/AppConfigPanel.vue @@ -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 diff --git a/app/src/views/app/AppInstall.vue b/app/src/views/app/AppInstall.vue index 8a3e3816..d19003cc 100644 --- a/app/src/views/app/AppInstall.vue +++ b/app/src/views/app/AppInstall.vue @@ -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 } diff --git a/app/src/views/domain/DomainConfig.vue b/app/src/views/domain/DomainConfig.vue index 5c15bcf1..8e31c593 100644 --- a/app/src/views/domain/DomainConfig.vue +++ b/app/src/views/domain/DomainConfig.vue @@ -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) }) }