import i18n from '@/i18n' import store from '@/store' import * as validators from '@/helpers/validators' import { isObjectLiteral, isEmptyValue, flattenObjectLiteral } from '@/helpers/commons' /** * Tries to find a translation corresponding to the user's locale/fallback locale in a * Yunohost argument or simply return the string if it's not an object literal. * * @param {(Object|String|undefined)} field - A field value containing a translation object or string * @return {String} */ export function formatI18nField (field) { if (typeof field === 'string') return field const { locale, fallbackLocale } = store.state return field ? field[locale] || field[fallbackLocale] || field.en : '' } /** * Returns a string size declaration to a M value. * * @param {String} sizeStr - A size declared like '500M' or '56k' * @return {Number} */ export function sizeToM (sizeStr) { const unit = sizeStr.slice(-1) const value = sizeStr.slice(0, -1) if (unit === 'M') return parseInt(value) if (unit === 'b') return Math.ceil(value / (1024 * 1024)) if (unit === 'k') return Math.ceil(value / 1024) if (unit === 'G') return Math.ceil(value * 1024) if (unit === 'T') return Math.ceil(value * 1024 * 1024) } /** * Returns a formatted address element to be used by AdressInputSelect component. * * @param {String} address - A string representing an adress (subdomain or email) * @return {Object} - `{ localPart, separator, domain }`. */ export function adressToFormValue (address) { const separator = address.includes('@') ? '@' : '.' const [localPart, domain] = address.split(separator) return { localPart, separator, domain } } /** * Format app install, actions and config panel argument into a data structure that * will be automaticly transformed into a component on screen. * * @param {Object} arg - a yunohost arg options written by a packager. * @return {Object} an formated argument containing formItem props, validation and base value. */ export function formatYunoHostArgument (arg) { let value = (arg.value !== undefined) ? arg.value : (arg.current_value !== undefined) ? arg.current_value : null const validation = {} const error = { message: null } arg.ask = formatI18nField(arg.ask) const field = { component: undefined, label: arg.ask, props: {} } const defaultProps = ['id:name', 'placeholder:example'] const components = [ { types: [undefined, 'string', 'path'], name: 'InputItem', props: defaultProps.concat(['autocomplete', 'trim', 'choices']), callback: function () { if (arg.choices) { arg.type = 'select' this.name = 'SelectItem' } } }, { types: ['email', 'url', 'date', 'time', 'color'], name: 'InputItem', props: defaultProps.concat(['type', 'trim']) }, { types: ['password'], name: 'InputItem', props: defaultProps.concat(['type', 'autocomplete', 'trim']), callback: function () { if (!arg.help) { arg.help = 'good_practices_about_admin_password' } arg.example = '••••••••••••' validation.passwordLenght = validators.minLength(8) } }, { types: ['number', 'range'], name: 'InputItem', props: defaultProps.concat(['type', 'min', 'max', 'step']), callback: function () { if (!isNaN(parseInt(arg.min))) { validation.minValue = validators.minValue(parseInt(arg.min)) } if (!isNaN(parseInt(arg.max))) { validation.maxValue = validators.maxValue(parseInt(arg.max)) } validation.numValue = validators.helpers.regex('Please provide an integer', new RegExp('^-?[0-9]+$')) } }, { types: ['select', 'user', 'domain'], name: 'SelectItem', props: ['id:name', 'choices'], callback: function () { if ((arg.type === 'domain') || (arg.type === 'user')) { field.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) } } } }, { types: ['file'], name: 'FileItem', props: defaultProps.concat(['accept']), callback: function () { if (value) { value = new File([''], value) value.currentfile = true } } }, { types: ['text'], name: 'TextAreaItem', props: defaultProps }, { types: ['tags'], name: 'TagsItem', props: defaultProps.concat(['limit', 'placeholder', 'options:choices', 'tagIcon:icon']), callback: function () { if (arg.choices) { this.name = 'TagsSelectizeItem' field.props.auto = true field.props.itemsName = '' field.props.label = arg.placeholder } if (typeof value === 'string') { value = value.split(',') } else if (!value) { value = [] } } }, { types: ['boolean'], name: 'CheckboxItem', props: ['id:name', 'choices'], callback: function () { if (value !== null && value !== undefined) { value = ['1', 'yes', 'y', 'true'].includes(String(value).toLowerCase()) } else if (arg.default !== null && arg.default !== undefined) { value = ['1', 'yes', 'y', 'true'].includes(String(arg.default).toLowerCase()) } } }, { types: ['alert'], name: 'ReadOnlyAlertItem', props: ['type:style', 'label:ask', 'icon'], readonly: true }, { types: ['markdown', 'display_text'], name: 'MarkdownItem', props: ['label:ask'], readonly: true } ] // Default type management if no one is filled if (arg.type === undefined) { arg.type = (arg.choices === undefined) ? 'string' : 'select' } // Search the component bind to the type const component = components.find(element => element.types.includes(arg.type)) if (component === undefined) throw new TypeError('Unknown type: ' + arg.type) // Callback use for specific behaviour if (component.callback) component.callback() field.component = component.name // Affect properties to the field Item for (let prop of component.props) { prop = prop.split(':') const propName = prop[0] const argName = prop.slice(-1)[0] if (argName in arg) { field.props[propName] = arg[argName] } } // We don't want to display a label html item as this kind or field contains // already the text to display if (component.readonly) delete field.label // Required (no need for checkbox its value can't be null) else if (field.component !== 'CheckboxItem' && arg.optional !== true) { validation.required = validators.required } if (arg.pattern && arg.type !== 'tags') { validation.pattern = validators.helpers.regex(formatI18nField(arg.pattern.error), new RegExp(arg.pattern.regexp)) } validation.remote = validators.helpers.withParams(error, (v) => { const result = !error.message error.message = null return result }) // field.props['title'] = field.pattern.error // Default value if still `null` if (value === null && arg.current_value) { value = arg.current_value } if (value === null && arg.default) { value = arg.default } // Help message if (arg.help) { field.description = formatI18nField(arg.help) } // Help message if (arg.helpLink) { field.link = { href: arg.helpLink.href, text: i18n.t(arg.helpLink.text) } } if (arg.visible) { field.visible = arg.visible // Temporary value to wait visible expression to be evaluated field.isVisible = true } return { value, field, // Return null instead of empty object if there's no validation validation: Object.keys(validation).length === 0 ? null : validation, error } } /** * Format app install, actions and config panel manifest args into a form that can be used * as v-model values, fields that can be passed to a FormField component and validations. * * @param {Array} args - a yunohost arg array written by a packager. * @param {String} name - (temp) an app name to build a label field in case of manifest install args * @return {Object} an object containing all parsed values to be used in vue views. */ export function formatYunoHostArguments (args, name = null) { const form = {} const fields = {} const validations = {} const errors = {} // FIXME yunohost should add the label field by default if (name) { args.unshift({ ask: i18n.t('label_for_manifestname', { name }), default: name, name: 'label' }) } for (const arg of args) { const { value, field, validation, error } = formatYunoHostArgument(arg) fields[arg.name] = field form[arg.name] = value if (validation) validations[arg.name] = validation errors[arg.name] = error } return { form, fields, validations, errors } } export function pFileReader (file, output, key, base64 = true) { return new Promise((resolve, reject) => { const fr = new FileReader() fr.onerror = reject fr.onload = () => { output[key] = fr.result if (base64) { output[key] = fr.result.replace(/data:[^;]*;base64,/, '') } output[key + '[name]'] = file.name resolve() } if (base64) { fr.readAsDataURL(file) } else { fr.readAsText(file) } }) } /** * Format helper for a form value. * Convert Boolean to (1|0) and concatenate adresses. * * @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('') } return value } /** * Format a form produced by a vue view to be sent to the server. * * @param {Object} formData - An object literal containing form values. * @param {Object} [extraParams] - Optionnal params * @param {Array} [extraParams.extract] - An array of keys that should be extracted from the form. * @param {Boolean} [extraParams.flatten=false] - Flattens or not the passed formData. * @param {Boolean} [extraParams.removeEmpty=true] - Removes "empty" values from the object. * @return {Object} the parsed data to be sent to the server, with extracted values if specified. */ export async function formatFormData ( formData, { extract = null, flatten = false, removeEmpty = true, removeNull = false, multipart = true } = {} ) { 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]) if (removeEmpty && isEmptyValue(value)) { continue } else if (removeNull && (value === null || value === undefined)) { 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 }