refactor: upgrade to vuelidate2

This commit is contained in:
axolotle 2024-03-04 03:37:39 +01:00
parent 81707bb11a
commit c5976dbd48
21 changed files with 246 additions and 177 deletions

View file

@ -16,6 +16,8 @@
"@fontsource/fira-code": "^4.5.13",
"@fontsource/firago": "^4.5.3",
"@vue/compat": "3.3.4",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"bootstrap-vue": "^2.22.0",
"date-fns": "^2.29.3",
"fork-awesome": "^1.2.0",

View file

@ -3,7 +3,7 @@
<RoutableTabs
v-if="routes_.length > 1"
:routes="routes_"
v-bind="{ panels, forms, v: $v, ...$attrs }"
v-bind="{ panels, forms, v: v$, ...$attrs }"
v-on="$listeners"
>
<template #tab-top>
@ -26,7 +26,8 @@
</template>
<script>
import { validationMixin } from 'vuelidate'
import { toRef } from 'vue'
import { useVuelidate } from '@vuelidate/core'
export default {
name: 'ConfigPanels',
@ -37,8 +38,6 @@ export default {
RoutableTabs: () => import('@/components/RoutableTabs.vue'),
},
mixins: [validationMixin],
props: {
panels: { type: Array, default: undefined },
forms: { type: Object, default: undefined },
@ -46,6 +45,14 @@ export default {
errors: { type: Object, default: undefined }, // never used
routes: { type: Array, default: null },
noRedirect: { type: Boolean, default: false },
externalResults: { type: Object, required: true },
},
setup(props) {
const externalResults = toRef(props, 'externalResults')
return {
v$: useVuelidate({ $externalResults: externalResults, $autoDirty: true }),
}
},
computed: {

View file

@ -47,7 +47,7 @@ export default {
computed: {
errorFeedback() {
if (this.serverError) return this.serverError
else if (this.validation && this.validation.$anyError) {
else if (this.validation && this.validation.$errors.length) {
return this.$i18n.t('form_errors.invalid_form')
} else return ''
},
@ -58,7 +58,7 @@ export default {
const v = this.validation
if (v) {
v.$touch()
if (v.$pending || v.$invalid) return
if (v.$pending || v.$errors.length) return
}
this.$emit('submit', e)
},

View file

@ -52,7 +52,7 @@ export default {
computed: {
errorFeedback() {
if (this.serverError) return this.serverError
else if (this.validation && this.validation.$anyError) {
else if (this.validation && this.validation.$errors.length) {
return this.$i18n.t('form_errors.invalid_form')
} else return ''
},

View file

@ -65,6 +65,7 @@ export default {
value: { type: null, default: null },
props: { type: Object, default: () => ({}) },
validation: { type: Object, default: null },
validationIndex: { type: Number, default: null },
},
computed: {
@ -93,19 +94,36 @@ export default {
return attrs
},
state() {
// Need to set state as null if no error, else component turn green
error() {
if (this.validation) {
return this.validation.$anyError === true ? false : null
if (this.validationIndex !== null) {
const errors =
this.validation.$each.$response.$errors[this.validationIndex]
const err = Object.values(errors).find((part) => {
return part.length
})
return err?.length ? err[0] : null
}
return this.validation.$errors.length
? { ...this.validation.$errors[0], $model: this.validation.$model }
: null
}
return null
},
state() {
// Need to set state as null if no error, else component turn green
return this.error ? false : null
},
errorMessage() {
const validation = this.validation
if (validation && validation.$anyError) {
const [type, errData] = this.findError(validation.$params, validation)
return this.$i18n.t('form_errors.' + type, errData)
const err = this.error
if (err) {
if (err.$message) return err.$message
return this.$i18n.t('form_errors.' + err.$validator, {
value: err.$model,
...err.$params,
})
}
return ''
},
@ -122,17 +140,6 @@ export default {
}
}
},
findError(params, obj, parent = obj) {
for (const key in params) {
if (!obj[key]) {
return [key, obj.$params[key]]
}
if (obj[key].$anyError) {
return this.findError(obj[key].$params, obj[key], parent)
}
}
},
},
}
</script>

View file

@ -1,70 +1,61 @@
import { helpers } from 'vuelidate/lib/validators'
import { helpers } from '@vuelidate/validators'
// Unicode ranges are taken from https://stackoverflow.com/a/37668315
const nonAsciiWordCharacters =
'\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC'
const alphalownumdot_ = helpers.regex('alphalownumdot_', /^[a-z0-9_.]+$/)
const alphalownumdot_ = helpers.regex(/^[a-z0-9_.]+$/)
const domain = helpers.regex(
'domain',
new RegExp(
`^(?:[\\da-z${nonAsciiWordCharacters}]+(?:-*[\\da-z${nonAsciiWordCharacters}]+)*\\.)+(?:(?:xn--)?[\\da-z${nonAsciiWordCharacters}]{2,})$`,
),
)
const dynDomain = helpers.regex(
'dynDomain',
new RegExp(`^(?:xn--)?[\\da-z-${nonAsciiWordCharacters}]+$`),
)
const emailLocalPart = helpers.regex('emailLocalPart', /^[\w.-]+$/)
const emailLocalPart = helpers.regex(/^[\w.-]+$/)
const emailForwardLocalPart = helpers.regex(
'emailForwardLocalPart',
/^[\w+.-]+$/,
)
const emailForwardLocalPart = helpers.regex(/^[\w+.-]+$/)
const email = (value) =>
helpers.withParams({ type: 'email', value }, (value) => {
const email = (value) => {
const [localPart, domainPart] = value.split('@')
if (!domainPart) return !helpers.req(value) || false
return (
!helpers.req(value) || (emailLocalPart(localPart) && domain(domainPart))
)
})(value)
}
// Same as email but with `+` allowed.
const emailForward = (value) =>
helpers.withParams({ type: 'emailForward', value }, (value) => {
const emailForward = (value) => {
const [localPart, domainPart] = value.split('@')
if (!domainPart) return !helpers.req(value) || false
return (
!helpers.req(value) ||
(emailForwardLocalPart(localPart) && domain(domainPart))
)
})(value)
}
const appRepoUrl = helpers.regex(
'appRepoUrl',
/^https:\/\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_./~]+\/[a-zA-Z0-9-_.]+_ynh(\/?(-\/)?tree\/[a-zA-Z0-9-_.]+)?(\.git)?\/?$/,
)
const includes = (items) => (item) =>
const includes = (items) =>
helpers.withParams(
{ type: 'includes', value: item },
{ type: 'includes' },
(item) => !helpers.req(item) || (items ? items.includes(item) : false),
)(item)
)
const name = helpers.regex(
'name',
new RegExp(`^(?:[A-Za-z${nonAsciiWordCharacters}]{1,30}[ ,.'-]{0,3})+$`),
)
const unique = (items) => (item) =>
helpers.withParams({ type: 'unique', arg: items, value: item }, (item) =>
const unique = (items) =>
helpers.withParams({ type: 'unique', arg: items }, (item) =>
items ? !helpers.req(item) || !items.includes(item) : true,
)(item)
)
export {
alphalownumdot_,

View file

@ -9,4 +9,4 @@ export {
minValue,
required,
sameAs,
} from 'vuelidate/lib/validators'
} from '@vuelidate/validators'

View file

@ -121,8 +121,7 @@ function addEvaluationGetter(prop, obj, expr, ctx, nested) {
*/
export function formatYunoHostArgument(arg) {
let value = arg.value !== undefined ? arg.value : null
const validation = {}
const error = { message: null }
let validation = {}
arg.ask = formatI18nField(arg.ask)
const field = {
is: arg.readonly ? 'ReadOnlyField' : 'FormField',
@ -272,6 +271,9 @@ export function formatYunoHostArgument(arg) {
]
// Default type management if no one is filled
if (arg.type !== 'tags' && arg.choices && arg.choices.length) {
arg.type = 'select'
}
if (arg.type === undefined) {
if (arg.choices && arg.choices.length) {
arg.type = 'select'
@ -308,21 +310,12 @@ export function formatYunoHostArgument(arg) {
validation.required = validators.required
}
if (arg.pattern && arg.type !== 'tags') {
validation.pattern = validators.helpers.regex(
validation.pattern = validators.helpers.withMessage(
formatI18nField(arg.pattern.error),
new RegExp(arg.pattern.regexp),
validators.helpers.regex(new RegExp(arg.pattern.regexp)),
)
}
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
})
}
// Default value if still `null`
if (value === null && arg.default) {
value = arg.default
@ -351,7 +344,6 @@ export function formatYunoHostArgument(arg) {
field,
// Return null instead of empty object if there's no validation
validation: Object.keys(validation).length === 0 ? null : validation,
error,
}
}
@ -367,14 +359,12 @@ export function formatYunoHostArguments(args, forms) {
const form = {}
const fields = {}
const validations = {}
const errors = {}
for (const arg of args) {
const { value, field, validation, error } = formatYunoHostArgument(arg)
const { value, field, validation } = formatYunoHostArgument(arg)
fields[arg.id] = field
form[arg.id] = value
if (validation) validations[arg.id] = validation
errors[arg.id] = error
if ('visible' in arg && typeof arg.visible === 'string') {
addEvaluationGetter(
@ -397,7 +387,7 @@ export function formatYunoHostArguments(args, forms) {
}
}
return { form, fields, validations, errors }
return { form, fields, validations }
}
export function formatYunoHostConfigPanels(data) {
@ -405,7 +395,6 @@ export function formatYunoHostConfigPanels(data) {
panels: [],
forms: {},
validations: {},
errors: {},
}
for (const { id: panelId, name, help, sections } of data.panels) {
@ -417,7 +406,6 @@ export function formatYunoHostConfigPanels(data) {
}
result.forms[panelId] = {}
result.validations[panelId] = {}
result.errors[panelId] = {}
if (name) panel.name = formatI18nField(name)
if (help) panel.help = formatI18nField(help)
@ -434,14 +422,13 @@ export function formatYunoHostConfigPanels(data) {
addEvaluationGetter('visible', section, section.visible, result.forms)
}
const { form, fields, validations, errors } = formatYunoHostArguments(
const { form, fields, validations } = 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)
Object.assign(result.errors[panelId], errors)
section.fields = fields
panel.sections.push(section)

View file

@ -2,7 +2,7 @@
<CardForm
:title="$t('login')"
icon="lock"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="login"
>
@ -10,14 +10,14 @@
<FormField
v-bind="fields.username"
v-model="form.username"
:validation="$v.form.username"
:validation="v$.form.username"
/>
<!-- ADMIN PASSWORD -->
<FormField
v-bind="fields.password"
v-model="form.password"
:validation="$v.form.password"
:validation="v$.form.password"
/>
<template #buttons>
@ -35,18 +35,22 @@
<script>
import { mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import { alphalownumdot_, required, minLength } from '@/helpers/validators'
export default {
name: 'LoginView',
mixins: [validationMixin],
props: {
forceReload: { type: Boolean, default: false },
},
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
serverError: '',

View file

@ -40,7 +40,7 @@
<CardForm
:title="$t('postinstall.user.title')"
icon="user-plus"
:validation="$v"
:validation="v$"
:server-error="serverError"
:submit-text="$t('next')"
@submit.prevent="setUser"
@ -55,7 +55,7 @@
:key="name"
v-bind="field"
v-model="user[name]"
:validation="$v.user[name]"
:validation="v$.user[name]"
/>
</CardForm>
@ -89,7 +89,7 @@
</template>
<script>
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import api from '@/api'
import { DomainForm } from '@/views/_partials'
@ -106,13 +106,17 @@ import {
export default {
name: 'PostInstall',
mixins: [validationMixin],
components: {
DomainForm,
LoginView,
},
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
step: 'start',
@ -224,7 +228,7 @@ export default {
username: { required, alphalownumdot_ },
fullname: { required, name },
password: { required, passwordLenght: minLength(8) },
confirmation: { required, passwordMatch: sameAs('password') },
confirmation: { required, passwordMatch: sameAs(this.user.password) },
},
}
},

View file

@ -3,7 +3,7 @@
:title="title"
icon="globe"
:submit-text="submitText"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="onSubmit"
>
@ -32,7 +32,7 @@
<FormField
v-bind="fields.domain"
v-model="form.domain"
:validation="$v.form.domain"
:validation="v$.form.domain"
class="mt-3"
/>
</BCollapse>
@ -58,7 +58,7 @@
<FormField
v-bind="fields.dynDomain"
:validation="$v.form.dynDomain"
:validation="v$.form.dynDomain"
class="mt-3"
>
<template #default="{ self }">
@ -68,13 +68,13 @@
<FormField
v-bind="fields.dynDomainPassword"
:validation="$v.form.dynDomainPassword"
:validation="v$.form.dynDomainPassword"
v-model="form.dynDomainPassword"
/>
<FormField
v-bind="fields.dynDomainPasswordConfirmation"
:validation="$v.form.dynDomainPasswordConfirmation"
:validation="v$.form.dynDomainPasswordConfirmation"
v-model="form.dynDomainPasswordConfirmation"
/>
</BCollapse>
@ -103,7 +103,7 @@
<FormField
v-bind="fields.localDomain"
:validation="$v.form.localDomain"
:validation="v$.form.localDomain"
class="mt-3"
>
<template #default="{ self }">
@ -116,7 +116,7 @@
<script>
import { mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import AdressInputSelect from '@/components/AdressInputSelect.vue'
import { formatFormData } from '@/helpers/yunohostArguments'
@ -137,6 +137,12 @@ export default {
serverError: { type: String, default: '' },
},
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
selected: '',
@ -262,8 +268,6 @@ export default {
}
},
mixins: [validationMixin],
components: {
AdressInputSelect,
},

View file

@ -171,7 +171,7 @@
icon="download"
@submit.prevent="onCustomInstallClick"
:submit-text="$t('install')"
:validation="$v"
:validation="v$"
class="mt-5"
>
<template #disclaimer>
@ -185,7 +185,7 @@
<FormField
v-bind="customInstall.field"
v-model="customInstall.url"
:validation="$v.customInstall.url"
:validation="v$.customInstall.url"
/>
</CardForm>
</template>
@ -220,7 +220,7 @@
</template>
<script>
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import CardDeckFeed from '@/components/CardDeckFeed.vue'
import { required, appRepoUrl } from '@/helpers/validators'
@ -240,6 +240,12 @@ export default {
subtag: { type: String, default: 'all' },
},
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
queries: [['GET', 'apps/catalog?full&with_categories&with_antifeatures']],
@ -421,8 +427,6 @@ export default {
randint,
},
mixins: [validationMixin],
}
</script>

View file

@ -128,7 +128,11 @@
</YAlert>
<!-- BASIC INFOS -->
<ConfigPanels v-bind="config" @submit="onConfigSubmit">
<ConfigPanels
v-bind="config"
:external-results="externalResults"
@submit="onConfigSubmit"
>
<!-- OPERATIONS TAB -->
<template v-if="currentTab === 'operations'" #tab-top>
<!-- CHANGE PERMISSIONS LABEL -->
@ -144,7 +148,7 @@
label-cols="0"
label-class=""
class="m-0"
:validation="$v.form.labels.$each[i]"
:validation="v$.form.labels.$each[i]"
>
<template #default="{ self }">
<BInputGroup>
@ -361,11 +365,11 @@
<script>
import { mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import api, { objectToParams } from '@/api'
import { humanPermissionName } from '@/helpers/filters/human'
import { required } from '@/helpers/validators'
import { helpers, required } from '@/helpers/validators'
import { isEmptyValue } from '@/helpers/commons'
import {
formatFormData,
@ -385,6 +389,12 @@ export default {
id: { type: String, required: true },
},
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
queries: [
@ -408,6 +418,7 @@ export default {
],
validations: {},
},
externalResults: {},
doc: undefined,
}
},
@ -429,7 +440,9 @@ export default {
return {
form: {
labels: {
$each: { label: { required } },
$each: helpers.forEach({
label: { required },
}),
},
url: { path: { required } },
},
@ -614,7 +627,9 @@ export default {
if (err.name !== 'APIBadRequestError') throw err
const panel = this.config.panels.find((panel) => panel.id === id)
if (err.data.name) {
this.config.errors[id][err.data.name].message = err.message
Object.assign(this.externalResults, {
forms: { [panel.id]: { [err.data.name]: [err.data.error] } },
})
} else this.$set(panel, 'serverError', err.message)
})
},
@ -687,8 +702,6 @@ export default {
})
},
},
mixins: [validationMixin],
}
</script>

View file

@ -179,7 +179,7 @@
:title="$t('app_install_parameters')"
icon="cog"
:submit-text="$t('install')"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="performInstall"
>
@ -189,7 +189,7 @@
:is="field.is"
v-bind="field.props"
v-model="form[fname]"
:validation="$v.form[fname]"
:validation="v$.form[fname]"
:key="fname"
/>
</template>
@ -210,7 +210,7 @@
</template>
<script>
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import api, { objectToParams } from '@/api'
import {
@ -223,8 +223,6 @@ import CardCollapse from '@/components/CardCollapse.vue'
export default {
name: 'AppInstall',
mixins: [validationMixin],
components: {
CardCollapse,
},
@ -233,6 +231,12 @@ export default {
id: { type: String, required: true },
},
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
queries: [

View file

@ -108,7 +108,12 @@
</DescriptionRow>
</YCard>
<ConfigPanels v-if="config.panels" v-bind="config" @submit="onConfigSubmit">
<ConfigPanels
v-if="config.panels"
v-bind="config"
:external-results="externalResults"
@submit="onConfigSubmit"
>
<template v-if="currentTab === 'dns'" #tab-after>
<DomainDns :name="name" />
</template>
@ -166,6 +171,7 @@ export default {
['GET', `domains/${this.name}/config?full`],
],
config: {},
externalResults: {},
unsubscribeDomainFromDyndns: false,
}
},
@ -247,7 +253,9 @@ export default {
if (err.name !== 'APIBadRequestError') throw err
const panel = this.config.panels.find((panel) => panel.id === id)
if (err.data.name) {
this.config.errors[id][err.data.name].message = err.message
Object.assign(this.externalResults, {
forms: { [panel.id]: { [err.data.name]: [err.data.error] } },
})
} else this.$set(panel, 'serverError', err.message)
})
},

View file

@ -2,7 +2,7 @@
<CardForm
:title="$t('group_new')"
icon="users"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="onSubmit"
>
@ -10,13 +10,13 @@
<FormField
v-bind="groupname"
v-model="form.groupname"
:validation="$v.form.groupname"
:validation="v$.form.groupname"
/>
</CardForm>
</template>
<script>
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import api from '@/api'
import { required, alphalownumdot_ } from '@/helpers/validators'
@ -24,6 +24,12 @@ import { required, alphalownumdot_ } from '@/helpers/validators'
export default {
name: 'GroupCreate',
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
form: {
@ -63,7 +69,5 @@ export default {
})
},
},
mixins: [validationMixin],
}
</script>

View file

@ -56,7 +56,7 @@
<CardForm
:title="$t('operations')"
icon="cogs"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="onFormPortToggling"
inline
@ -66,7 +66,7 @@
<BFormSelect v-model="form.action" :options="actionChoices" />
</BInputGroup>
<FormField :validation="$v.form.port">
<FormField :validation="v$.form.port">
<BInputGroup :prepend="$t('port')">
<InputItem
id="input-port"
@ -119,7 +119,7 @@
</template>
<script>
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import api from '@/api'
import { required, integer, between } from '@/helpers/validators'
@ -127,6 +127,12 @@ import { required, integer, between } from '@/helpers/validators'
export default {
name: 'ToolFirewall',
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
queries: [['GET', '/firewall?raw']],
@ -281,8 +287,6 @@ export default {
})
},
},
mixins: [validationMixin],
}
</script>

View file

@ -8,6 +8,7 @@
<ConfigPanels
v-if="config.panels"
v-bind="config"
:external-results="externalResults"
@submit="onConfigSubmit"
/>
</ViewBase>
@ -34,6 +35,7 @@ export default {
return {
queries: [['GET', 'settings?full']],
config: {},
externalResults: {},
}
},
@ -62,7 +64,9 @@ export default {
if (err.name !== 'APIBadRequestError') throw err
const panel = this.config.panels.find((panel) => panel.id === id)
if (err.data.name) {
this.config.errors[id][err.data.name].message = err.message
Object.assign(this.externalResults, {
forms: { [panel.id]: { [err.data.name]: [err.data.error] } },
})
} else this.$set(panel, 'serverError', err.message)
})
},

View file

@ -7,7 +7,7 @@
<CardForm
:title="$t('users_new')"
icon="user-plus"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="onSubmit"
>
@ -15,19 +15,19 @@
<FormField
v-bind="fields.username"
v-model="form.username"
:validation="$v.form.username"
:validation="v$.form.username"
/>
<!-- USER FULLNAME -->
<FormField
v-bind="fields.fullname"
:validation="$v.form.fullname"
:validation="v$.form.fullname"
v-model="form.fullname"
/>
<hr />
<!-- USER MAIL DOMAIN -->
<FormField v-bind="fields.domain" :validation="$v.form.domain">
<FormField v-bind="fields.domain" :validation="v$.form.domain">
<template #default="{ self }">
<BInputGroup>
<BInputGroupAppend>
@ -55,14 +55,14 @@
<FormField
v-bind="fields.password"
v-model="form.password"
:validation="$v.form.password"
:validation="v$.form.password"
/>
<!-- USER PASSWORD CONFIRMATION -->
<FormField
v-bind="fields.confirmation"
v-model="form.confirmation"
:validation="$v.form.confirmation"
:validation="v$.form.confirmation"
/>
</CardForm>
</ViewBase>
@ -71,7 +71,7 @@
<script>
import api from '@/api'
import { mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import { formatFormData } from '@/helpers/yunohostArguments'
import {
@ -86,6 +86,12 @@ import {
export default {
name: 'UserCreate',
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
queries: [
@ -164,7 +170,7 @@ export default {
fullname: { required, name },
domain: { required },
password: { required, passwordLenght: minLength(8) },
confirmation: { required, passwordMatch: sameAs('password') },
confirmation: { required, passwordMatch: sameAs(this.form.password) },
},
}
},
@ -191,8 +197,6 @@ export default {
})
},
},
mixins: [validationMixin],
}
</script>

View file

@ -7,7 +7,7 @@
<CardForm
:title="$t('user_username_edit', { name })"
icon="user"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="onSubmit"
>
@ -18,13 +18,13 @@
<FormField
v-bind="fields.fullname"
v-model="form.fullname"
:validation="$v.form.fullname"
:validation="v$.form.fullname"
/>
<hr />
<!-- USER EMAIL -->
<FormField v-bind="fields.mail" :validation="$v.form.mail">
<FormField v-bind="fields.mail" :validation="v$.form.mail">
<template #default="{ self }">
<AdressInputSelect v-bind="self" v-model="form.mail" />
</template>
@ -33,7 +33,7 @@
<!-- MAILBOX QUOTA -->
<FormField
v-bind="fields.mailbox_quota"
:validation="$v.form.mailbox_quota"
:validation="v$.form.mailbox_quota"
>
<template #default="{ self }">
<BInputGroup append="M">
@ -49,7 +49,8 @@
<FormField
v-bind="fields.mail_aliases"
:id="'mail_aliases' + i"
:validation="$v.form.mail_aliases.$each[i]"
:validation="v$.form.mail_aliases"
:validation-index="i"
>
<template #default="{ self }">
<AdressInputSelect v-bind="self" v-model="form.mail_aliases[i]" />
@ -72,9 +73,10 @@
<div v-for="(mail, i) in form.mail_forward" :key="i" class="mail-list">
<FormField
v-bind="fields.mail_forward"
v-model="form.mail_forward[i]"
v-model="form.mail_forward[i].mail"
:id="'mail-forward' + i"
:validation="$v.form.mail_forward.$each[i]"
:validation="v$.form.mail_forward"
:validation-index="i"
/>
<BButton variant="danger" @click="removeEmailField('forward', i)">
@ -93,14 +95,14 @@
<FormField
v-bind="fields.change_password"
v-model="form.change_password"
:validation="$v.form.change_password"
:validation="v$.form.change_password"
/>
<!-- USER PASSWORD CONFIRMATION -->
<FormField
v-bind="fields.confirmation"
v-model="form.confirmation"
:validation="$v.form.confirmation"
:validation="v$.form.confirmation"
/>
</CardForm>
</ViewBase>
@ -108,7 +110,7 @@
<script>
import { mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import api from '@/api'
import { arrayDiff } from '@/helpers/commons'
@ -118,6 +120,7 @@ import {
formatFormData,
} from '@/helpers/yunohostArguments'
import {
helpers,
name,
required,
minLength,
@ -137,6 +140,12 @@ export default {
name: { type: String, required: true },
},
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
queries: [
@ -227,7 +236,8 @@ export default {
computed: mapGetters(['user', 'domainsAsChoices', 'mainDomain']),
validations: {
validations() {
return {
form: {
fullname: { required, name },
mail: {
@ -235,16 +245,19 @@ export default {
},
mailbox_quota: { integer, minValue: minValue(0) },
mail_aliases: {
$each: {
$each: helpers.forEach({
localPart: { required, email: emailLocalPart },
},
}),
},
mail_forward: {
$each: { required, emailForward },
$each: helpers.forEach({
mail: { required, emailForward },
}),
},
change_password: { passwordLenght: minLength(8) },
confirmation: { passwordMatch: sameAs('change_password') },
confirmation: { passwordMatch: sameAs(this.form.change_password) },
},
}
},
methods: {
@ -260,7 +273,7 @@ export default {
)
}
if (user['mail-forward']) {
this.form.mail_forward = user['mail-forward'].slice() // Copy value
this.form.mail_forward = user['mail-forward'].map((mail) => ({ mail })) // Copy value
}
// mailbox-quota could be 'No quota' or 'Pas de quota'...
if (parseInt(user['mailbox-quota'].limit) > 0) {
@ -278,6 +291,8 @@ export default {
formData.mailbox_quota = ''
}
formData.mail_forward = formData.mail_forward?.map((v) => v.mail)
for (const key of ['mail_aliases', 'mail_forward']) {
const dashedKey = key.replace('_', '-')
const newKey = key.replace('_', '').replace('es', '')
@ -323,7 +338,7 @@ export default {
this.form['mail_' + type].push(
type === 'aliases'
? { localPart: '', separator: '@', domain: this.mainDomain }
: '',
: { mail: '' },
)
// Focus last input after rendering update
this.$nextTick(() => {
@ -337,7 +352,6 @@ export default {
},
},
mixins: [validationMixin],
components: { AdressInputSelect },
}
</script>

View file

@ -2,7 +2,7 @@
<CardForm
:title="$t('users_import')"
icon="user-plus"
:validation="$v"
:validation="v$"
:server-error="serverError"
@submit.prevent="onSubmit"
>
@ -10,7 +10,7 @@
<FormField
v-bind="fields.csvfile"
v-model="form.csvfile"
:validation="$v.form.csvfile"
:validation="v$.form.csvfile"
/>
<!-- UPDATE -->
@ -23,7 +23,7 @@
<script>
import api from '@/api'
import { validationMixin } from 'vuelidate'
import { useVuelidate } from '@vuelidate/core'
import { formatFormData } from '@/helpers/yunohostArguments'
import { required } from '@/helpers/validators'
@ -31,6 +31,12 @@ import { required } from '@/helpers/validators'
export default {
name: 'UserImport',
setup() {
return {
v$: useVuelidate(),
}
},
data() {
return {
form: {
@ -112,7 +118,5 @@ export default {
})
},
},
mixins: [validationMixin],
}
</script>