diff --git a/app/src/components/ConfigPanel.vue b/app/src/components/ConfigPanel.vue
index be92c993..78d6d1a5 100644
--- a/app/src/components/ConfigPanel.vue
+++ b/app/src/components/ConfigPanel.vue
@@ -1,7 +1,7 @@
@@ -12,18 +12,22 @@
-
+
{{ section.name }} {{ section.help }}
-
-
+
@@ -31,7 +35,7 @@
-
-
diff --git a/app/src/components/globals/FormField.vue b/app/src/components/globals/FormField.vue
index d7be17e1..3dd85173 100644
--- a/app/src/components/globals/FormField.vue
+++ b/app/src/components/globals/FormField.vue
@@ -28,15 +28,18 @@
-
{{ link.text }}
-
@@ -76,8 +79,8 @@ export default {
if ('label' in attrs) {
const defaultAttrs = {
'label-cols-md': 4,
- 'label-cols-lg': 2,
- 'label-class': 'font-weight-bold'
+ 'label-cols-lg': 3,
+ 'label-class': ['font-weight-bold', 'py-0']
}
if (!('label-cols' in attrs)) {
for (const attr in defaultAttrs) {
diff --git a/app/src/components/globals/ReadOnlyField.vue b/app/src/components/globals/ReadOnlyField.vue
new file mode 100644
index 00000000..107165b5
--- /dev/null
+++ b/app/src/components/globals/ReadOnlyField.vue
@@ -0,0 +1,65 @@
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/components/globals/formItems/ButtonItem.vue b/app/src/components/globals/formItems/ButtonItem.vue
new file mode 100644
index 00000000..c09f3fb4
--- /dev/null
+++ b/app/src/components/globals/formItems/ButtonItem.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/components/globals/formItems/DisplayTextItem.vue b/app/src/components/globals/formItems/DisplayTextItem.vue
new file mode 100644
index 00000000..186ef1a7
--- /dev/null
+++ b/app/src/components/globals/formItems/DisplayTextItem.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/components/globals/formItems/FileItem.vue b/app/src/components/globals/formItems/FileItem.vue
index 8dca3b7b..160c2fae 100644
--- a/app/src/components/globals/formItems/FileItem.vue
+++ b/app/src/components/globals/formItems/FileItem.vue
@@ -1,19 +1,24 @@
-
+
+ {{ $t('delete') }}
+
@@ -21,18 +26,14 @@
diff --git a/app/src/components/globals/formItems/MarkdownItem.vue b/app/src/components/globals/formItems/MarkdownItem.vue
index 7fd4bacb..359c4e67 100644
--- a/app/src/components/globals/formItems/MarkdownItem.vue
+++ b/app/src/components/globals/formItems/MarkdownItem.vue
@@ -12,4 +12,3 @@ export default {
}
}
-
diff --git a/app/src/components/globals/formItems/ReadOnlyAlertItem.vue b/app/src/components/globals/formItems/ReadOnlyAlertItem.vue
index bc63ff6d..ac1dac55 100644
--- a/app/src/components/globals/formItems/ReadOnlyAlertItem.vue
+++ b/app/src/components/globals/formItems/ReadOnlyAlertItem.vue
@@ -1,8 +1,10 @@
-
-
-
+
+
+
@@ -11,29 +13,23 @@
export default {
name: 'ReadOnlyAlertItem',
- data () {
- const icons = {
- success: 'thumbs-up',
- info: 'info-circle',
- warning: 'warning',
- danger: 'times'
- }
- return {
- icon_: (this.icon) ? this.icon : icons[this.type]
- }
- },
-
props: {
id: { type: String, default: null },
label: { type: String, default: null },
type: { type: String, default: null },
icon: { type: String, default: null }
+ },
+
+ computed: {
+ icon_ () {
+ const icons = {
+ success: 'thumbs-up',
+ info: 'info',
+ warning: 'exclamation',
+ danger: 'times'
+ }
+ return this.icon || icons[this.type]
+ }
}
}
-
-
diff --git a/app/src/components/globals/formItems/TextAreaItem.vue b/app/src/components/globals/formItems/TextAreaItem.vue
index b9224f10..95af03e8 100644
--- a/app/src/components/globals/formItems/TextAreaItem.vue
+++ b/app/src/components/globals/formItems/TextAreaItem.vue
@@ -1,6 +1,6 @@
filter(...args)))
+}
+
+
/**
* Returns an new array containing items that are in first array but not in the other.
*
@@ -100,3 +113,26 @@ export function escapeHtml (unsafe) {
export function randint (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
+
+
+/**
+ * Returns a File content.
+ *
+ * @param {File} file
+ * @param {Object} [extraParams] - Optionnal params
+ * @param {Boolean} [extraParams.base64] - returns a base64 representation of the file.
+ * @return {Promise}
+ */
+export function getFileContent (file, { base64 = false } = {}) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader()
+ reader.onerror = reject
+ reader.onload = () => resolve(reader.result)
+
+ if (base64) {
+ reader.readAsDataURL(file)
+ } else {
+ reader.readAsText(file)
+ }
+ })
+}
diff --git a/app/src/helpers/yunohostArguments.js b/app/src/helpers/yunohostArguments.js
index e5b01202..ffded7a5 100644
--- a/app/src/helpers/yunohostArguments.js
+++ b/app/src/helpers/yunohostArguments.js
@@ -2,7 +2,12 @@ import i18n from '@/i18n'
import store from '@/store'
import evaluate from 'simple-evaluate'
import * as validators from '@/helpers/validators'
-import { isObjectLiteral, isEmptyValue, flattenObjectLiteral } from '@/helpers/commons'
+import {
+ isObjectLiteral,
+ isEmptyValue,
+ flattenObjectLiteral,
+ getFileContent
+} from '@/helpers/commons'
/**
@@ -49,6 +54,49 @@ export function adressToFormValue (address) {
}
+/**
+ * Evaluate config panel string expression that can contain regular expressions.
+ * Expression are evaluated with the config panel form as context.
+ *
+ * @param {String} expression - A String to evaluate.
+ * @param {Object} forms - A nested form used in config panels.
+ * @return {Boolean} - expression evaluation result.
+ */
+export function evaluateExpression (expression, forms) {
+ if (!expression) return true
+ if (expression === '"false"') return false
+
+ const context = Object.values(forms).reduce((ctx, args) => {
+ Object.entries(args).forEach(([name, value]) => {
+ ctx[name] = isObjectLiteral(value) && 'file' in value ? value.content : value
+ })
+ return ctx
+ }, {})
+
+ // Allow to use match(var,regexp) function
+ const matchRe = new RegExp('match\\(\\s*(\\w+)\\s*,\\s*"([^"]+)"\\s*\\)', 'g')
+ for (const matched of expression.matchAll(matchRe)) {
+ const [fullMatch, varMatch, regExpMatch] = matched
+ const varName = varMatch + '__re' + matched.index
+ context[varName] = new RegExp(regExpMatch, 'm').test(context[varMatch])
+ expression = expression.replace(fullMatch, varName)
+ }
+
+ try {
+ return !!evaluate(context, expression)
+ } catch {
+ return false
+ }
+}
+
+// Adds a property to an Object that will dynamically returns a expression evaluation result.
+function addEvaluationGetter (prop, obj, expr, ctx) {
+ Object.defineProperty(obj, prop, {
+ get: () => evaluateExpression(expr, ctx)
+ })
+}
+
+
/**
* Format app install, actions and config panel argument into a data structure that
* will be automaticly transformed into a component on screen.
@@ -62,22 +110,21 @@ export function formatYunoHostArgument (arg) {
const error = { message: null }
arg.ask = formatI18nField(arg.ask)
const field = {
- component: undefined,
- label: arg.ask,
- props: {}
+ is: arg.readonly ? 'ReadOnlyField' : 'FormField',
+ visible: [undefined, true, '"true"'].includes(arg.visible),
+ props: {
+ label: arg.ask,
+ component: undefined,
+ props: {}
+ }
}
+
const defaultProps = ['id:name', 'placeholder:example']
const components = [
{
- types: [undefined, 'string', 'path'],
+ types: ['string', 'path'],
name: 'InputItem',
- props: defaultProps.concat(['autocomplete', 'trim', 'choices']),
- callback: function () {
- if (arg.choices && Object.keys(arg.choices).length) {
- arg.type = 'select'
- this.name = 'SelectItem'
- }
- }
+ props: defaultProps.concat(['autocomplete', 'trim', 'choices'])
},
{
types: ['email', 'url', 'date', 'time', 'color'],
@@ -115,9 +162,9 @@ export function formatYunoHostArgument (arg) {
name: 'SelectItem',
props: ['id:name', 'choices'],
callback: function () {
- if ((arg.type !== 'select')) {
- field.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) }
- }
+ if (arg.type !== 'select') {
+ field.props.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) }
+ }
}
},
{
@@ -125,9 +172,12 @@ export function formatYunoHostArgument (arg) {
name: 'FileItem',
props: defaultProps.concat(['accept']),
callback: function () {
- if (value) {
- value = new File([''], value)
- value.currentfile = true
+ value = {
+ // in case of already defined file, we receive only the file path (not the actual file)
+ file: value ? new File([''], value) : null,
+ content: '',
+ current: !!value,
+ removed: false
}
}
},
@@ -141,11 +191,13 @@ export function formatYunoHostArgument (arg) {
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 (arg.choices && arg.choices.length) {
+ this.name = 'TagsSelectizeItem'
+ Object.assign(field.props.props, {
+ auto: true,
+ itemsName: '',
+ label: arg.placeholder
+ })
}
if (typeof value === 'string') {
value = value.split(',')
@@ -170,53 +222,67 @@ export function formatYunoHostArgument (arg) {
types: ['alert'],
name: 'ReadOnlyAlertItem',
props: ['type:style', 'label:ask', 'icon'],
- readonly: true
+ renderSelf: true
},
{
- types: ['markdown', 'display_text'],
+ types: ['markdown'],
name: 'MarkdownItem',
props: ['label:ask'],
- readonly: true
+ renderSelf: true
+ },
+ {
+ types: ['display_text'],
+ name: 'DisplayTextItem',
+ props: ['label:ask'],
+ renderSelf: true
+ },
+ {
+ types: ['button'],
+ name: 'ButtonItem',
+ props: ['type:style', 'label:ask', 'icon', 'enabled'],
+ renderSelf: true
}
]
// Default type management if no one is filled
if (arg.type === undefined) {
- arg.type = (arg.choices === undefined) ? 'string' : 'select'
+ arg.type = arg.choices && arg.choices.length ? 'select' : 'string'
}
+
// 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
+ field.props.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]
+ field.props.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) {
+ if (!component.renderSelf && arg.type !== 'boolean' && 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
- })
+ if (!component.renderSelf && !arg.readonly) {
+ // Bind a validation with what the server may respond
+ 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
@@ -227,18 +293,17 @@ export function formatYunoHostArgument (arg) {
// Help message
if (arg.help) {
- field.description = formatI18nField(arg.help)
+ field.props.description = formatI18nField(arg.help)
}
// Help message
if (arg.helpLink) {
- field.link = { href: arg.helpLink.href, text: i18n.t(arg.helpLink.text) }
+ field.props.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
+ if (component.renderSelf) {
+ field.is = field.props.component
+ field.props = field.props.props
}
return {
@@ -256,30 +321,29 @@ export function formatYunoHostArgument (arg) {
* 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
+ * @param {Object|null} forms - nested form used as the expression evualuations context.
* @return {Object} an object containing all parsed values to be used in vue views.
*/
-export function formatYunoHostArguments (args, name = null) {
+export function formatYunoHostArguments (args, forms) {
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
+
+ if ('visible' in arg && ![false, '"false"'].includes(arg.visible)) {
+ addEvaluationGetter('visible', field, arg.visible, forms)
+ }
+
+ if ('enabled' in arg) {
+ addEvaluationGetter('enabled', field.props, arg.enabled, forms)
+ }
}
return { form, fields, validations, errors }
@@ -303,11 +367,24 @@ export function formatYunoHostConfigPanels (data) {
if (name) panel.name = formatI18nField(name)
if (help) panel.help = formatI18nField(help)
- for (const { id: sectionId, name, help, visible, options } of sections) {
- const section = { id: sectionId, visible, isVisible: false }
- if (help) section.help = formatI18nField(help)
- if (name) section.name = formatI18nField(name)
- const { form, fields, validations, errors } = formatYunoHostArguments(options)
+ for (const _section of sections) {
+ const section = {
+ id: _section.id,
+ isActionSection: _section.is_action_section,
+ visible: [undefined, true, '"true"'].includes(_section.visible)
+ }
+ if (_section.help) section.help = formatI18nField(_section.help)
+ if (_section.name) section.name = formatI18nField(_section.name)
+ if (_section.visible && ![false, '"false"'].includes(_section.visible)) {
+ addEvaluationGetter('visible', section, _section.visible, result.forms)
+ }
+
+ const {
+ form,
+ fields,
+ validations,
+ errors
+ } = formatYunoHostArguments(_section.options, result.forms)
// Merge all sections forms to the panel to get a unique form
Object.assign(result.forms[panelId], form)
Object.assign(result.validations[panelId], validations)
@@ -323,80 +400,65 @@ export function formatYunoHostConfigPanels (data) {
}
-export function configPanelsFieldIsVisible (expression, field, forms) {
- if (!expression || !field) return true
- const context = {}
-
- const promises = []
- for (const args of Object.values(forms)) {
- for (const shortname in args) {
- if (args[shortname] instanceof File) {
- if (expression.includes(shortname)) {
- promises.push(pFileReader(args[shortname], context, shortname, false))
- }
- } else {
- context[shortname] = args[shortname]
- }
- }
- }
-
- // Allow to use match(var,regexp) function
- const matchRe = new RegExp('match\\(\\s*(\\w+)\\s*,\\s*"([^"]+)"\\s*\\)', 'g')
- let i = 0
- Promise.all(promises).then(() => {
- for (const matched of expression.matchAll(matchRe)) {
- i++
- const varName = matched[1] + '__re' + i.toString()
- context[varName] = new RegExp(matched[2], 'm').test(context[matched[1]])
- expression = expression.replace(matched[0], varName)
- }
-
- try {
- field.isVisible = evaluate(context, expression)
- } catch {
- field.isVisible = false
- }
- })
-
- return field.isVisible
-}
-
-
-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.
+ * 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 || 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 }), {})
+ })
}
@@ -412,38 +474,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/i18n/locales/en.json b/app/src/i18n/locales/en.json
index 518b5c92..12acc54f 100644
--- a/app/src/i18n/locales/en.json
+++ b/app/src/i18n/locales/en.json
@@ -378,13 +378,14 @@
"human_routes": {
"adminpw": "Change admin password",
"apps": {
+ "action_config": "Run action '{action}' of app '{name}' configuration",
"change_label": "Change label of '{prevName}' for '{nextName}'",
"change_url": "Change access URL of '{name}'",
"install": "Install app '{name}'",
"set_default": "Redirect '{domain}' domain root to '{name}'",
"perform_action": "Perform action '{action}' of app '{name}'",
"uninstall": "Uninstall app '{name}'",
- "update_config": "Update app '{name}' configuration"
+ "update_config": "Update panel '{id}' of app '{name}' configuration"
},
"backups": {
"create": "Create a backup",
@@ -406,13 +407,11 @@
"domains": {
"add": "Add domain '{name}'",
"delete": "Delete domain '{name}'",
- "install_LE": "Install certificate for '{name}'",
- "manual_renew_LE": "Renew certificate for '{name}'",
+ "cert_install": "Install certificate for '{name}'",
+ "cert_renew": "Renew certificate for '{name}'",
"push_dns_changes": "Push DNS records to registrar for '{name}'",
- "regen_selfsigned": "Renew self-signed certificate for '{name}'",
- "revert_to_selfsigned": "Revert to self-signed certificate for '{name}'",
"set_default": "Set '{name}' as default domain",
- "update_config": "Update '{name}' configuration"
+ "update_config": "Update panel '{id}' of domain '{name}' configuration"
},
"firewall": {
"ports": "{action} port {port} ({protocol}, {connection})",
@@ -548,37 +547,16 @@
"words": {
"browse": "Browse",
"collapse": "Collapse",
- "default": "Default"
+ "default": "Default",
+ "none": "None",
+ "separator": ", "
},
"wrong_password": "Wrong password",
"yes": "Yes",
"yunohost_admin": "YunoHost Admin",
- "certificate_alert_not_valid": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!",
- "certificate_alert_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!",
- "certificate_alert_letsencrypt_about_to_expire": "Current certificate is about to expire. It should soon be renewed automatically.",
- "certificate_alert_about_to_expire": "WARNING: Current certificate is about to expire! It will NOT be renewed automatically!",
- "certificate_alert_good": "Okay, current certificate looks good!",
- "certificate_alert_great": "Great! You're using a valid Let's Encrypt certificate!",
- "certificate_alert_unknown": "Unknown status",
"certificate_manage": "Manage SSL certificate",
"ssl_certificate": "SSL certificate",
- "confirm_cert_install_LE": "Are you sure you want to install a Let's Encrypt certificate for this domain?",
- "confirm_cert_regen_selfsigned": "Are you sure you want to regenerate a self-signed certificate for this domain?",
- "confirm_cert_manual_renew_LE": "Are you sure you want to manually renew the Let's Encrypt certificate for this domain now?",
- "confirm_cert_revert_to_selfsigned": "Are you sure you want to revert this domain to a self-signed certificate?",
"certificate": "Certificate",
- "certificate_status": "Certificate status",
- "certificate_authority": "Certification authority",
- "validity": "Validity",
- "domain_is_eligible_for_ACME": "This domain seems correctly configured to install a Let's Encrypt certificate!",
- "domain_not_eligible_for_ACME": "This domain doesn't seem ready for a Let's Encrypt certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in the diagnosis page can help you understand what is misconfigured.",
- "install_letsencrypt_cert": "Install a Let's Encrypt certificate",
- "manually_renew_letsencrypt_message": "Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).",
- "manually_renew_letsencrypt": "Manually renew now",
- "regenerate_selfsigned_cert_message": "If you want, you can regenerate the self-signed certificate.",
- "regenerate_selfsigned_cert": "Regenerate self-signed certificate",
- "revert_to_selfsigned_cert_message": "If you really want to, you can reinstall a self-signed certificate. (Not recommended)",
- "revert_to_selfsigned_cert": "Revert to a self-signed certificate",
"purge_user_data_checkbox": "Purge {name}'s data? (This will remove the content of its home and mail directories.)",
"purge_user_data_warning": "Purging user's data is not reversible. Be sure you know what you're doing!"
}
diff --git a/app/src/router/routes.js b/app/src/router/routes.js
index 4253307d..1dd6fa1b 100644
--- a/app/src/router/routes.js
+++ b/app/src/router/routes.js
@@ -179,16 +179,6 @@ const routes = [
breadcrumb: ['domain-list', 'domain-info', 'domain-dns']
}
},
- {
- name: 'domain-cert',
- path: '/domains/:name/cert-management',
- component: () => import(/* webpackChunkName: "views/domain/cert" */ '@/views/domain/DomainCert'),
- props: true,
- meta: {
- args: { trad: 'certificate' },
- breadcrumb: ['domain-list', 'domain-info', 'domain-cert']
- }
- },
/* ───────╮
│ APPS │
diff --git a/app/src/scss/main.scss b/app/src/scss/main.scss
index b87594cb..f1fe207a 100644
--- a/app/src/scss/main.scss
+++ b/app/src/scss/main.scss
@@ -102,6 +102,11 @@ body {
}
}
+h3.card-title {
+ margin-bottom: 1em;
+ border-bottom: solid 1px #aaa;
+}
+
// collapse icon
.not-collapsed > .icon {
transform: rotate(-90deg);
@@ -165,6 +170,10 @@ body {
}
}
+.alert p:last-child {
+ margin-bottom: 0;
+}
+
code {
background: ghostwhite;
}
diff --git a/app/src/views/_partials/DomainForm.vue b/app/src/views/_partials/DomainForm.vue
index b7a6dfe6..b52e2e94 100644
--- a/app/src/views/_partials/DomainForm.vue
+++ b/app/src/views/_partials/DomainForm.vue
@@ -126,12 +126,10 @@ export default {
},
methods: {
- onSubmit () {
+ async onSubmit () {
const domainType = this.selected
- this.$emit('submit', {
- domain: formatFormDataValue(this.form[domainType]),
- domainType
- })
+ const domain = await formatFormDataValue(this.form[domainType])
+ this.$emit('submit', { domain, domainType })
}
},
diff --git a/app/src/views/app/AppConfigPanel.vue b/app/src/views/app/AppConfigPanel.vue
index cb619241..ed9f1562 100644
--- a/app/src/views/app/AppConfigPanel.vue
+++ b/app/src/views/app/AppConfigPanel.vue
@@ -3,7 +3,10 @@
:queries="queries" @queries-response="onQueriesResponse"
ref="view" skeleton="card-form-skeleton"
>
-
+
{{ $t('app_config_panel_no_panel') }}
@@ -34,7 +37,7 @@ export default {
data () {
return {
queries: [
- ['GET', `apps/${this.id}/config-panel?full`]
+ ['GET', `apps/${this.id}/config?full`]
],
config: {}
}
@@ -49,23 +52,22 @@ export default {
}
},
- async applyConfig (id_) {
- const formatedData = await formatFormData(
- this.config.forms[id_],
- { removeEmpty: false, removeNull: true, multipart: false }
- )
+ async onConfigSubmit ({ id, form, action, name }) {
+ const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
api.put(
- `apps/${this.id}/config`,
- { key: id_, args: objectToParams(formatedData) },
- { key: 'apps.update_config', name: this.id }
- ).then(response => {
+ action
+ ? `apps/${this.id}/actions/${action}`
+ : `apps/${this.id}/config/${id}`,
+ { args: objectToParams(args) },
+ { key: `apps.${action ? 'action' : 'update'}_config`, id, name: this.id }
+ ).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)
})
}
diff --git a/app/src/views/app/AppInstall.vue b/app/src/views/app/AppInstall.vue
index db0a27b6..6cfa3fa0 100644
--- a/app/src/views/app/AppInstall.vue
+++ b/app/src/views/app/AppInstall.vue
@@ -24,10 +24,9 @@
@submit.prevent="performInstall"
>
-
@@ -47,10 +46,13 @@
diff --git a/app/src/views/domain/DomainConfig.vue b/app/src/views/domain/DomainConfig.vue
index 5c15bcf1..83b1da98 100644
--- a/app/src/views/domain/DomainConfig.vue
+++ b/app/src/views/domain/DomainConfig.vue
@@ -3,7 +3,10 @@
:queries="queries" @queries-response="onQueriesResponse"
ref="view" skeleton="card-form-skeleton"
>
-
+
@@ -41,23 +44,27 @@ export default {
this.config = formatYunoHostConfigPanels(config)
},
- async applyConfig (id_) {
- const formatedData = await formatFormData(
- this.config.forms[id_],
- { removeEmpty: false, removeNull: true, multipart: false }
- )
+ async onConfigSubmit ({ id, form, action, name }) {
+ const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
+ const call = action
+ ? api.put(
+ `domain/${this.name}/actions/${action}`,
+ { args: objectToParams(args) },
+ { key: 'domains.' + name, name: this.name }
+ )
+ : api.put(
+ `domains/${this.name}/config/${id}`,
+ { args: objectToParams(args) },
+ { key: 'domains.update_config', id, name: this.name }
+ )
- api.put(
- `domains/${this.name}/config`,
- { key: id_, args: objectToParams(formatedData) },
- { key: 'domains.update_config', name: this.name }
- ).then(() => {
+ call.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)
})
}
diff --git a/app/src/views/domain/DomainInfo.vue b/app/src/views/domain/DomainInfo.vue
index ed7cb2ac..3f52e27a 100644
--- a/app/src/views/domain/DomainInfo.vue
+++ b/app/src/views/domain/DomainInfo.vue
@@ -32,13 +32,6 @@
-
- {{ $t('certificate_manage') }}
-
- {{ $t('ssl_certificate') }}
-
-
-
{{ $t('domain_delete_longdesc') }}