mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
refactor: use useForm in some views
This commit is contained in:
parent
a22ee09344
commit
026ccb68ed
8 changed files with 465 additions and 569 deletions
|
@ -1,12 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter, type LocationQueryValue } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
import { useForm } from '@/composables/form'
|
||||
import { alphalownumdot_, minLength, required } from '@/helpers/validators'
|
||||
import { useStoreGetters } from '@/store/utils'
|
||||
import type { FieldProps, FormFieldDict } from '@/types/form'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -22,41 +23,40 @@ const store = useStore()
|
|||
const router = useRouter()
|
||||
|
||||
const { installed } = useStoreGetters()
|
||||
const serverError = ref('')
|
||||
const form = reactive({
|
||||
|
||||
type Form = typeof form.value
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
const v$ = useVuelidate(
|
||||
{
|
||||
username: { required, alphalownumdot_ },
|
||||
password: { required, passwordLenght: minLength(4) },
|
||||
},
|
||||
form,
|
||||
)
|
||||
|
||||
console.log(v$.value)
|
||||
|
||||
const fields = {
|
||||
const fields = reactive({
|
||||
username: {
|
||||
component: 'InputItem',
|
||||
label: t('user_username'),
|
||||
rules: { required, alphalownumdot_ },
|
||||
props: {
|
||||
id: 'username',
|
||||
autocomplete: 'username',
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['username']>,
|
||||
|
||||
password: {
|
||||
component: 'InputItem',
|
||||
label: t('password'),
|
||||
rules: { required, passwordLenght: minLength(4) },
|
||||
props: {
|
||||
id: 'password',
|
||||
type: 'password',
|
||||
autocomplete: 'current-password',
|
||||
},
|
||||
},
|
||||
}
|
||||
} satisfies FieldProps<'InputItem', Form['password']>,
|
||||
} satisfies FormFieldDict<Form>)
|
||||
|
||||
function login() {
|
||||
const credentials = [form.username, form.password].join(':')
|
||||
const { v, onSubmit } = useForm(form, fields)
|
||||
|
||||
const onLogin = onSubmit((onError) => {
|
||||
const { username, password } = form.value
|
||||
const credentials = [username, password].join(':')
|
||||
store
|
||||
.dispatch('LOGIN', credentials)
|
||||
.then(() => {
|
||||
|
@ -70,41 +70,27 @@ function login() {
|
|||
)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name !== 'APIUnauthorizedError') throw err
|
||||
serverError.value = t('wrong_password_or_username')
|
||||
})
|
||||
}
|
||||
.catch((err) => onError(err, t('wrong_password_or_username')))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardForm
|
||||
:title="t('login')"
|
||||
id="login-form"
|
||||
v-model="form"
|
||||
:fields="fields"
|
||||
icon="lock"
|
||||
:validation="v$"
|
||||
:server-error="serverError"
|
||||
@submit.prevent="login"
|
||||
:title="t('login')"
|
||||
:validations="v"
|
||||
@submit="onLogin"
|
||||
>
|
||||
<!-- ADMIN USERNAME -->
|
||||
<FormField
|
||||
v-bind="fields.username"
|
||||
v-model="form.username"
|
||||
:validation="v$.username"
|
||||
/>
|
||||
|
||||
<!-- ADMIN PASSWORD -->
|
||||
<FormField
|
||||
v-bind="fields.password"
|
||||
v-model="form.password"
|
||||
:validation="v$.password"
|
||||
/>
|
||||
|
||||
<template #buttons>
|
||||
<!-- FIXME should we remove the disabled state? -->
|
||||
<BButton
|
||||
type="submit"
|
||||
variant="success"
|
||||
:disabled="!installed"
|
||||
form="ynh-form"
|
||||
form="login-form"
|
||||
>
|
||||
{{ t('login') }}
|
||||
</BButton>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import api from '@/api'
|
||||
import { APIBadRequestError } from '@/api/errors'
|
||||
import { useForm } from '@/composables/form'
|
||||
import { useAutoModal } from '@/composables/useAutoModal'
|
||||
import { asUnreffed } from '@/helpers/commons'
|
||||
import {
|
||||
alphalownumdot_,
|
||||
minLength,
|
||||
|
@ -13,92 +15,99 @@ import {
|
|||
sameAs,
|
||||
} from '@/helpers/validators'
|
||||
import { formatFormData } from '@/helpers/yunohostArguments'
|
||||
import type { FieldProps, FormFieldDict } from '@/types/form'
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
import { DomainForm } from '@/views/_partials'
|
||||
|
||||
const { t } = useI18n()
|
||||
const modalConfirm = useAutoModal()
|
||||
|
||||
const step = ref('start')
|
||||
type Steps = 'start' | 'domain' | 'user' | 'rootfsspace-error' | 'login'
|
||||
const step = ref<Steps>('start')
|
||||
const serverError = ref('')
|
||||
const domain = ref(undefined)
|
||||
const domain = ref('')
|
||||
const dyndns_recovery_password = ref('')
|
||||
|
||||
const form = reactive({
|
||||
type Form = typeof form.value
|
||||
const form = ref({
|
||||
username: '',
|
||||
fullname: '',
|
||||
password: '',
|
||||
confirmation: '',
|
||||
})
|
||||
const rules = computed(() => ({
|
||||
username: { required, alphalownumdot_ },
|
||||
fullname: { required, name },
|
||||
password: { required, passwordLenght: minLength(8) },
|
||||
confirmation: { required, passwordMatch: sameAs(form.password) },
|
||||
}))
|
||||
const v$ = useVuelidate(rules, form)
|
||||
const fields = reactive({
|
||||
// FIXME satisfies FormFieldDict but not for CardForm?
|
||||
alert: {
|
||||
component: 'ReadOnlyAlertItem',
|
||||
props: { label: t('postinstall.user.first_user_help'), type: 'info' },
|
||||
} satisfies FieldProps<'ReadOnlyAlertItem'>,
|
||||
|
||||
const fields = {
|
||||
username: {
|
||||
component: 'InputItem',
|
||||
label: t('user_username'),
|
||||
props: {
|
||||
id: 'username',
|
||||
placeholder: t('placeholder.username'),
|
||||
},
|
||||
},
|
||||
rules: { required, alphalownumdot_ },
|
||||
props: { id: 'username', placeholder: t('placeholder.username') },
|
||||
} satisfies FieldProps<'InputItem', Form['username']>,
|
||||
|
||||
fullname: {
|
||||
component: 'InputItem',
|
||||
label: t('user_fullname'),
|
||||
props: {
|
||||
id: 'fullname',
|
||||
placeholder: t('placeholder.fullname'),
|
||||
},
|
||||
},
|
||||
rules: { required, name },
|
||||
props: { id: 'fullname', placeholder: t('placeholder.fullname') },
|
||||
} satisfies FieldProps<'InputItem', Form['fullname']>,
|
||||
|
||||
password: {
|
||||
component: 'InputItem',
|
||||
label: t('password'),
|
||||
description: t('good_practices_about_admin_password'),
|
||||
descriptionVariant: 'warning',
|
||||
rules: { required, passwordLenght: minLength(8) },
|
||||
props: { id: 'password', placeholder: '••••••••', type: 'password' },
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['password']>,
|
||||
|
||||
confirmation: {
|
||||
component: 'InputItem',
|
||||
label: t('password_confirmation'),
|
||||
props: {
|
||||
id: 'confirmation',
|
||||
placeholder: '••••••••',
|
||||
type: 'password',
|
||||
},
|
||||
},
|
||||
}
|
||||
rules: asUnreffed(
|
||||
computed(() => ({
|
||||
required,
|
||||
passwordMatch: sameAs(form.value.password),
|
||||
})),
|
||||
),
|
||||
props: { id: 'confirmation', placeholder: '••••••••', type: 'password' },
|
||||
} satisfies FieldProps<'InputItem', Form['confirmation']>,
|
||||
} satisfies FormFieldDict<Form>)
|
||||
|
||||
function goToStep(step_) {
|
||||
const { v, onSubmit, serverErrors } = useForm(form, fields)
|
||||
|
||||
function goToStep(step_: Steps) {
|
||||
serverError.value = ''
|
||||
step.value = step_
|
||||
}
|
||||
|
||||
function setDomain(data) {
|
||||
function setDomain(data: { domain: string; dyndns_recovery_password: string }) {
|
||||
domain.value = data.domain
|
||||
dyndns_recovery_password.value = data.dyndns_recovery_password
|
||||
goToStep('user')
|
||||
}
|
||||
|
||||
async function setUser() {
|
||||
const setUser = onSubmit(async () => {
|
||||
const confirmed = await modalConfirm(
|
||||
t('confirm_postinstall', { domain: domain.value }),
|
||||
)
|
||||
if (!confirmed) return
|
||||
performPostInstall()
|
||||
}
|
||||
})
|
||||
|
||||
async function performPostInstall(force = false) {
|
||||
// FIXME update formatFormData to unwrap ref auto
|
||||
const { username, fullname, password } = form.value
|
||||
const data = await formatFormData({
|
||||
domain: domain.value,
|
||||
dyndns_recovery_password: dyndns_recovery_password.value,
|
||||
username: form.username,
|
||||
fullname: form.fullname,
|
||||
password: form.password,
|
||||
username,
|
||||
fullname,
|
||||
password,
|
||||
})
|
||||
|
||||
// FIXME does the api will throw an error for bad passwords ?
|
||||
|
@ -111,19 +120,21 @@ async function performPostInstall(force = false) {
|
|||
goToStep('login')
|
||||
})
|
||||
.catch((err) => {
|
||||
const hasWordsInError = (words) =>
|
||||
const hasWordsInError = (words: string[]) =>
|
||||
words.some((word) => (err.key || err.message).includes(word))
|
||||
if (err.name !== 'APIBadRequestError') throw err
|
||||
if (!(err instanceof APIBadRequestError)) throw err
|
||||
if (err.key === 'postinstall_low_rootfsspace') {
|
||||
step.value = 'rootfsspace-error'
|
||||
serverError.value = err.message
|
||||
} else if (hasWordsInError(['domain', 'dyndns'])) {
|
||||
step.value = 'domain'
|
||||
serverError.value = err.message
|
||||
} else if (hasWordsInError(['password', 'user'])) {
|
||||
step.value = 'user'
|
||||
serverErrors.global = [err.message]
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
serverError.value = err.message
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
@ -156,11 +167,11 @@ async function performPostInstall(force = false) {
|
|||
@submit="setDomain"
|
||||
>
|
||||
<template #disclaimer>
|
||||
<p class="alert alert-info" v-t="'postinstall_domain'" />
|
||||
<p v-t="'postinstall_domain'" class="alert alert-info" />
|
||||
</template>
|
||||
</DomainForm>
|
||||
|
||||
<BButton variant="primary" @click="goToStep('start')" class="mt-3">
|
||||
<BButton variant="primary" class="mt-3" @click="goToStep('start')">
|
||||
<YIcon iname="chevron-left" /> {{ $t('previous') }}
|
||||
</BButton>
|
||||
</template>
|
||||
|
@ -168,28 +179,16 @@ async function performPostInstall(force = false) {
|
|||
<!-- FIRST USER SETUP STEP -->
|
||||
<template v-else-if="step === 'user'">
|
||||
<CardForm
|
||||
:title="$t('postinstall.user.title')"
|
||||
v-model="form"
|
||||
:fields="fields"
|
||||
icon="user-plus"
|
||||
:validation="v$"
|
||||
:server-error="serverError"
|
||||
:submit-text="$t('next')"
|
||||
:title="$t('postinstall.user.title')"
|
||||
:validations="v"
|
||||
@submit.prevent="setUser"
|
||||
>
|
||||
<ReadOnlyAlertItem
|
||||
:label="$t('postinstall.user.first_user_help')"
|
||||
type="info"
|
||||
/>
|
||||
|
||||
<FormField
|
||||
v-for="(field, key) in fields"
|
||||
:key="key"
|
||||
v-bind="field"
|
||||
v-model="form[key]"
|
||||
:validation="v$.form[key]"
|
||||
/>
|
||||
</CardForm>
|
||||
|
||||
<BButton variant="primary" @click="goToStep('domain')" class="mt-3">
|
||||
<BButton variant="primary" class="mt-3" @click="goToStep('domain')">
|
||||
<YIcon iname="chevron-left" /> {{ $t('previous') }}
|
||||
</BButton>
|
||||
</template>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useForm } from '@/composables/form'
|
||||
import { asUnreffed } from '@/helpers/commons'
|
||||
import {
|
||||
domain,
|
||||
dynDomain,
|
||||
|
@ -12,23 +13,26 @@ import {
|
|||
} from '@/helpers/validators'
|
||||
import { formatFormData } from '@/helpers/yunohostArguments'
|
||||
import { useStoreGetters } from '@/store/utils'
|
||||
import type { AdressModelValue, FieldProps, FormFieldDict } from '@/types/form'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
submitText?: string | null
|
||||
submitText?: string
|
||||
serverError?: string
|
||||
}>(),
|
||||
{
|
||||
submitText: null,
|
||||
serverError: '',
|
||||
submitText: undefined,
|
||||
serverError: undefined,
|
||||
},
|
||||
)
|
||||
const emit = defineEmits(['submit'])
|
||||
const emit = defineEmits<{
|
||||
submit: [data: { domain: string; dyndns_recovery_password: string }]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
@ -42,82 +46,112 @@ const dynDnsForbiden = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
const selected = ref(dynDnsForbiden.value ? 'domain' : '')
|
||||
const form = reactive({
|
||||
type Selected = 'domain' | 'dynDomain' | 'localDomain'
|
||||
const selected = ref<'' | Selected>(dynDnsForbiden.value ? 'domain' : '')
|
||||
|
||||
type Form = {
|
||||
domain: string
|
||||
dynDomain: AdressModelValue
|
||||
dynDomainPassword: string
|
||||
dynDomainPasswordConfirmation: string
|
||||
localDomain: AdressModelValue
|
||||
}
|
||||
const form = ref<Form>({
|
||||
domain: '',
|
||||
dynDomain: { localPart: '', separator: '.', domain: 'nohost.me' },
|
||||
dynDomainPassword: '',
|
||||
dynDomainPasswordConfirmation: '',
|
||||
localDomain: { localPart: '', separator: '.', domain: 'local' },
|
||||
})
|
||||
|
||||
const rules = computed(() => ({
|
||||
selected: { required },
|
||||
form: ['domain', 'localDomain'].includes(selected.value)
|
||||
? {
|
||||
[selected.value]:
|
||||
selected.value === 'domain'
|
||||
? { required, domain }
|
||||
: { localPart: { required, dynDomain } },
|
||||
}
|
||||
: {
|
||||
dynDomain: { localPart: { required, dynDomain } },
|
||||
dynDomainPassword: { passwordLenght: minLength(8) },
|
||||
dynDomainPasswordConfirmation: {
|
||||
passwordMatch: sameAs(form.dynDomainPassword),
|
||||
},
|
||||
},
|
||||
}))
|
||||
const v$ = useVuelidate(rules, { selected, form })
|
||||
|
||||
const fields = {
|
||||
const fields = reactive({
|
||||
domain: {
|
||||
component: 'InputItem',
|
||||
label: t('domain_name'),
|
||||
rules: asUnreffed(
|
||||
computed(() =>
|
||||
selected.value === 'domain' ? { required, domain } : undefined,
|
||||
),
|
||||
),
|
||||
props: {
|
||||
id: 'domain',
|
||||
placeholder: t('placeholder.domain'),
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['domain']>,
|
||||
|
||||
dynDomain: {
|
||||
component: 'AdressItem',
|
||||
label: t('domain_name'),
|
||||
rules: { localPart: { required, dynDomain } },
|
||||
props: {
|
||||
id: 'dyn-domain',
|
||||
placeholder: t('placeholder.domain').split('.')[0],
|
||||
type: 'domain',
|
||||
choices: dynDomains,
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'AdressItem', Form['dynDomain']>,
|
||||
|
||||
dynDomainPassword: {
|
||||
component: 'InputItem',
|
||||
label: t('domain.add.dyn_dns_password'),
|
||||
description: t('domain.add.dyn_dns_password_desc'),
|
||||
rules: { passwordLenght: minLength(8) },
|
||||
props: {
|
||||
id: 'dyn-dns-password',
|
||||
placeholder: '••••••••',
|
||||
type: 'password',
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['dynDomainPassword']>,
|
||||
|
||||
dynDomainPasswordConfirmation: {
|
||||
component: 'InputItem',
|
||||
label: t('password_confirmation'),
|
||||
rules: asUnreffed(
|
||||
computed(() => ({
|
||||
passwordMatch: sameAs(form.value.dynDomainPassword),
|
||||
})),
|
||||
),
|
||||
props: {
|
||||
id: 'dyn-dns-password-confirmation',
|
||||
placeholder: '••••••••',
|
||||
type: 'password',
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['dynDomainPasswordConfirmation']>,
|
||||
|
||||
localDomain: {
|
||||
component: 'AdressItem',
|
||||
label: t('domain_name'),
|
||||
rules: asUnreffed(
|
||||
computed(() =>
|
||||
selected.value === 'localDomain'
|
||||
? { localPart: { required, dynDomain } }
|
||||
: undefined,
|
||||
),
|
||||
),
|
||||
props: {
|
||||
id: 'dyn-domain',
|
||||
placeholder: t('placeholder.domain').split('.')[0],
|
||||
type: 'domain',
|
||||
choices: ['local', 'test'],
|
||||
},
|
||||
} satisfies FieldProps<'AdressItem', Form['localDomain']>,
|
||||
} satisfies FormFieldDict<Form>)
|
||||
|
||||
const { v, onSubmit, serverErrors } = useForm(form, fields)
|
||||
|
||||
watch(
|
||||
() => props.serverError,
|
||||
() => {
|
||||
if (props.serverError) {
|
||||
serverErrors.global = [props.serverError]
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const dynKeys = [
|
||||
'dynDomain',
|
||||
'dynDomainPassword',
|
||||
'dynDomainPasswordConfirmation',
|
||||
] as (keyof Form)[]
|
||||
|
||||
const domainIsVisible = computed(() => {
|
||||
return selected.value === 'domain'
|
||||
|
@ -131,15 +165,17 @@ const localDomainIsVisible = computed(() => {
|
|||
return selected.value === 'localDomain'
|
||||
})
|
||||
|
||||
async function onSubmit() {
|
||||
const onDomainAdd = onSubmit(async () => {
|
||||
const domainType = selected.value
|
||||
if (!domainType) return
|
||||
|
||||
const data = await formatFormData({
|
||||
domain: form[domainType],
|
||||
domain: form.value[domainType],
|
||||
dyndns_recovery_password:
|
||||
domainType === 'dynDomain' ? form.dynDomainPassword : '',
|
||||
domainType === 'dynDomain' ? form.value.dynDomainPassword : '',
|
||||
})
|
||||
emit('submit', data)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -147,9 +183,8 @@ async function onSubmit() {
|
|||
:title="title"
|
||||
icon="globe"
|
||||
:submit-text="submitText"
|
||||
:validation="v$"
|
||||
:server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
:validations="v"
|
||||
@submit.prevent="onDomainAdd"
|
||||
>
|
||||
<template #disclaimer>
|
||||
<slot name="disclaimer" />
|
||||
|
@ -167,8 +202,8 @@ async function onSubmit() {
|
|||
{{ $t('domain.add.from_registrar') }}
|
||||
</BFormRadio>
|
||||
|
||||
<BCollapse id="collapse-domain" v-model:visible="domainIsVisible">
|
||||
<p class="mt-2 alert alert-info">
|
||||
<BCollapse id="collapse-domain" v-model="domainIsVisible">
|
||||
<p class="mt-2 mb-3 alert alert-info">
|
||||
<YIcon iname="info-circle" />
|
||||
<span class="ps-1" v-html="$t('domain.add.from_registrar_desc')" />
|
||||
</p>
|
||||
|
@ -176,8 +211,7 @@ async function onSubmit() {
|
|||
<FormField
|
||||
v-bind="fields.domain"
|
||||
v-model="form.domain"
|
||||
:validation="v$.domain"
|
||||
class="mt-3"
|
||||
:validation="v.domain"
|
||||
/>
|
||||
</BCollapse>
|
||||
|
||||
|
@ -194,34 +228,21 @@ async function onSubmit() {
|
|||
{{ $t('domain.add.from_yunohost') }}
|
||||
</BFormRadio>
|
||||
|
||||
<BCollapse id="collapse-dynDomain" v-model:visible="dynDomainIsVisible">
|
||||
<p class="mt-2 alert alert-info">
|
||||
<BCollapse id="collapse-dynDomain" v-model="dynDomainIsVisible">
|
||||
<p class="mt-2 mb-3 alert alert-info">
|
||||
<YIcon iname="info-circle" />
|
||||
<span class="ps-1" v-html="$t('domain.add.from_yunohost_desc')" />
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
v-bind="fields.dynDomain"
|
||||
:validation="v$.dynDomain"
|
||||
class="mt-3"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<AdressItem v-bind="self" v-model="form.dynDomain" />
|
||||
</template>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-bind="fields.dynDomainPassword"
|
||||
:validation="v$.dynDomainPassword"
|
||||
v-model="form.dynDomainPassword"
|
||||
/>
|
||||
|
||||
<FormField
|
||||
v-bind="fields.dynDomainPasswordConfirmation"
|
||||
:validation="v$.dynDomainPasswordConfirmation"
|
||||
v-model="form.dynDomainPasswordConfirmation"
|
||||
v-for="key in dynKeys"
|
||||
:key="key"
|
||||
v-bind="fields[key]"
|
||||
v-model="form[key]"
|
||||
:validation="v[key]"
|
||||
/>
|
||||
</BCollapse>
|
||||
|
||||
<div
|
||||
v-if="dynDnsForbiden"
|
||||
class="alert alert-warning mt-2"
|
||||
|
@ -239,21 +260,17 @@ async function onSubmit() {
|
|||
{{ $t('domain.add.from_local') }}
|
||||
</BFormRadio>
|
||||
|
||||
<BCollapse id="collapse-localDomain" v-model:visible="localDomainIsVisible">
|
||||
<p class="mt-2 alert alert-info">
|
||||
<BCollapse id="collapse-localDomain" v-model="localDomainIsVisible">
|
||||
<p class="mt-2 mb-3 alert alert-info">
|
||||
<YIcon iname="info-circle" />
|
||||
<span class="ps-1" v-html="$t('domain.add.from_local_desc')" />
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
v-bind="fields.localDomain"
|
||||
:validation="v$.localDomain"
|
||||
class="mt-3"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<AdressItem v-bind="self" v-model="form.localDomain" />
|
||||
</template>
|
||||
</FormField>
|
||||
v-model="form.localDomain"
|
||||
:validation="v.localDomain"
|
||||
/>
|
||||
</BCollapse>
|
||||
</CardForm>
|
||||
</template>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import CardDeckFeed from '@/components/CardDeckFeed.vue'
|
||||
import { useForm } from '@/composables/form'
|
||||
import { useAutoModal } from '@/composables/useAutoModal'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { randint } from '@/helpers/commons'
|
||||
import { appRepoUrl, required } from '@/helpers/validators'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import type { FieldProps, FormFieldDict } from '@/types/form'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -37,8 +38,20 @@ const { loading } = useInitialQueries(
|
|||
const apps = ref()
|
||||
const selectedApp = ref()
|
||||
const antifeatures = ref()
|
||||
const url = ref()
|
||||
const v$ = useVuelidate({ url: { required, appRepoUrl } }, { url })
|
||||
|
||||
const form = ref({ url: '' })
|
||||
const fields = {
|
||||
url: {
|
||||
component: 'InputItem',
|
||||
label: t('url'),
|
||||
rules: { required, appRepoUrl },
|
||||
props: {
|
||||
id: 'custom-install',
|
||||
placeholder: 'https://some.git.forge.tld/USER/REPOSITORY',
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', string>,
|
||||
} satisfies FormFieldDict<typeof form.value>
|
||||
const { v, onSubmit } = useForm(form, fields)
|
||||
|
||||
const qualityOptions = [
|
||||
{ value: 'high_quality', text: t('only_highquality_apps') },
|
||||
|
@ -169,16 +182,16 @@ async function onInstallClick(appId: string) {
|
|||
}
|
||||
|
||||
// INSTALL CUSTOM APP
|
||||
async function onCustomInstallClick() {
|
||||
const onCustomInstallClick = onSubmit(async () => {
|
||||
const confirmed = await modalConfirm(t('confirm_install_custom_app'))
|
||||
if (!confirmed) return
|
||||
|
||||
const url_ = url.value
|
||||
const url = form.value.url
|
||||
router.push({
|
||||
name: 'app-install-custom',
|
||||
params: { id: url_.endsWith('/') ? url_ : url_ + '/' },
|
||||
params: { id: url.endsWith('/') ? url : url + '/' },
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -354,12 +367,14 @@ async function onCustomInstallClick() {
|
|||
<template #bot>
|
||||
<!-- INSTALL CUSTOM APP -->
|
||||
<CardForm
|
||||
:title="$t('custom_app_install')"
|
||||
v-model="form"
|
||||
icon="download"
|
||||
@submit.prevent="onCustomInstallClick"
|
||||
:fields="fields"
|
||||
:submit-text="$t('install')"
|
||||
:validation="v$"
|
||||
:title="$t('custom_app_install')"
|
||||
:validations="v"
|
||||
class="mt-5"
|
||||
@submit.prevent="onCustomInstallClick"
|
||||
>
|
||||
<template #disclaimer>
|
||||
<div class="alert alert-warning">
|
||||
|
@ -367,13 +382,6 @@ async function onCustomInstallClick() {
|
|||
{{ $t('confirm_install_custom_app') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- URL -->
|
||||
<FormField
|
||||
v-bind="customInstall.field"
|
||||
v-model="customInstall.url"
|
||||
:validation="v$.customInstall.url"
|
||||
/>
|
||||
</CardForm>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,56 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import api from '@/api'
|
||||
import { APIBadRequestError, APIError } from '@/api/errors'
|
||||
import { useForm } from '@/composables/form'
|
||||
import { alphalownumdot_, required } from '@/helpers/validators'
|
||||
import type { FieldProps, FormFieldDict } from '@/types/form'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const form = reactive({ groupname: '' })
|
||||
const v$ = useVuelidate({ groupname: { required, alphalownumdot_ } }, form)
|
||||
const serverError = ref('')
|
||||
const groupnameField = {
|
||||
const form = ref({ groupname: '' })
|
||||
const fields = {
|
||||
groupname: {
|
||||
component: 'InputItem',
|
||||
label: t('group_name'),
|
||||
description: t('group_format_name_help'),
|
||||
rules: { required, alphalownumdot_ },
|
||||
props: {
|
||||
id: 'groupname',
|
||||
placeholder: t('placeholder.groupname'),
|
||||
},
|
||||
}
|
||||
function onSubmit() {
|
||||
} satisfies FieldProps<'InputItem', string>,
|
||||
} satisfies FormFieldDict<typeof form.value>
|
||||
|
||||
const { v, onSubmit } = useForm(form, fields)
|
||||
|
||||
const onAddGroup = onSubmit((onError) => {
|
||||
api
|
||||
.post({ uri: 'users/groups', storeKey: 'groups' }, form, {
|
||||
key: 'groups.create',
|
||||
name: form.groupname,
|
||||
name: form.value.groupname,
|
||||
})
|
||||
.then(() => {
|
||||
router.push({ name: 'group-list' })
|
||||
})
|
||||
.catch((err: APIError) => {
|
||||
if (!(err instanceof APIBadRequestError)) throw err
|
||||
serverError.value = err.message
|
||||
})
|
||||
}
|
||||
.catch(onError)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardForm
|
||||
:title="$t('group_new')"
|
||||
v-model="form"
|
||||
icon="users"
|
||||
:validation="v$"
|
||||
:server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<!-- GROUP NAME -->
|
||||
<FormField
|
||||
v-bind="groupnameField"
|
||||
v-model="form.groupname"
|
||||
:validation="v$.form.groupname"
|
||||
:fields="fields"
|
||||
:title="$t('group_new')"
|
||||
:validations="v"
|
||||
@submit.prevent="onAddGroup"
|
||||
/>
|
||||
</CardForm>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import api from '@/api'
|
||||
import { APIBadRequestError, type APIError } from '@/api/errors'
|
||||
import { useForm } from '@/composables/form'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { asUnreffed } from '@/helpers/commons'
|
||||
import {
|
||||
alphalownumdot_,
|
||||
minLength,
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
} from '@/helpers/validators'
|
||||
import { formatFormData } from '@/helpers/yunohostArguments'
|
||||
import { useStoreGetters } from '@/store/utils'
|
||||
import type { FieldProps, FormFieldDict } from '@/types/form'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
@ -30,149 +31,122 @@ const { loading } = useInitialQueries(
|
|||
|
||||
const { userNames, domainsAsChoices, mainDomain } = useStoreGetters()
|
||||
|
||||
const fields = {
|
||||
username: {
|
||||
label: t('user_username'),
|
||||
props: {
|
||||
id: 'username',
|
||||
placeholder: t('placeholder.username'),
|
||||
},
|
||||
},
|
||||
|
||||
fullname: {
|
||||
label: t('user_fullname'),
|
||||
props: {
|
||||
id: 'fullname',
|
||||
placeholder: t('placeholder.fullname'),
|
||||
},
|
||||
},
|
||||
|
||||
domain: {
|
||||
id: 'mail',
|
||||
label: t('user_email'),
|
||||
description: t('tip_about_user_email'),
|
||||
descriptionVariant: 'info',
|
||||
props: { choices: domainsAsChoices },
|
||||
},
|
||||
|
||||
password: {
|
||||
label: t('password'),
|
||||
description: t('good_practices_about_user_password'),
|
||||
descriptionVariant: 'warning',
|
||||
props: {
|
||||
id: 'password',
|
||||
placeholder: '••••••••',
|
||||
type: 'password',
|
||||
},
|
||||
},
|
||||
|
||||
confirmation: {
|
||||
label: t('password_confirmation'),
|
||||
props: {
|
||||
id: 'confirmation',
|
||||
placeholder: '••••••••',
|
||||
type: 'password',
|
||||
},
|
||||
},
|
||||
}
|
||||
const form = reactive({
|
||||
type Form = typeof form.value
|
||||
const form = ref({
|
||||
username: '',
|
||||
fullname: '',
|
||||
domain: '',
|
||||
password: '',
|
||||
confirmation: '',
|
||||
})
|
||||
const rules = computed(() => ({
|
||||
const fields = {
|
||||
username: {
|
||||
component: 'InputItem',
|
||||
label: t('user_username'),
|
||||
rules: asUnreffed(
|
||||
computed(() => ({
|
||||
required,
|
||||
alphalownumdot_,
|
||||
notInUsers: unique(userNames.value),
|
||||
notInUsers: unique(userNames),
|
||||
})),
|
||||
),
|
||||
props: {
|
||||
id: 'username',
|
||||
placeholder: t('placeholder.username'),
|
||||
},
|
||||
fullname: { required, name },
|
||||
domain: { required },
|
||||
password: { required, passwordLenght: minLength(8) },
|
||||
confirmation: { required, passwordMatch: sameAs(form.password) },
|
||||
}))
|
||||
const v$ = useVuelidate(rules, form)
|
||||
const serverError = ref('')
|
||||
} satisfies FieldProps<'InputItem', Form['username']>,
|
||||
|
||||
fullname: {
|
||||
component: 'InputItem',
|
||||
hr: true,
|
||||
label: t('user_fullname'),
|
||||
rules: { required, name },
|
||||
props: {
|
||||
id: 'fullname',
|
||||
placeholder: t('placeholder.fullname'),
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['fullname']>,
|
||||
|
||||
domain: {
|
||||
component: 'SelectItem',
|
||||
hr: true,
|
||||
id: 'mail',
|
||||
label: t('user_email'),
|
||||
description: t('tip_about_user_email'),
|
||||
descriptionVariant: 'info',
|
||||
rules: { required },
|
||||
props: { choices: asUnreffed(domainsAsChoices) },
|
||||
} satisfies FieldProps<'SelectItem', Form['domain']>,
|
||||
|
||||
password: {
|
||||
component: 'InputItem',
|
||||
label: t('password'),
|
||||
description: t('good_practices_about_user_password'),
|
||||
descriptionVariant: 'warning',
|
||||
rules: { required, passwordLenght: minLength(8) },
|
||||
props: {
|
||||
id: 'password',
|
||||
placeholder: '••••••••',
|
||||
type: 'password',
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['password']>,
|
||||
|
||||
confirmation: {
|
||||
component: 'InputItem',
|
||||
label: t('password_confirmation'),
|
||||
rules: asUnreffed(
|
||||
computed(() => ({
|
||||
required,
|
||||
passwordMatch: sameAs(form.value.password),
|
||||
})),
|
||||
),
|
||||
props: {
|
||||
id: 'confirmation',
|
||||
placeholder: '••••••••',
|
||||
type: 'password',
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['confirmation']>,
|
||||
} satisfies FormFieldDict<Form>
|
||||
|
||||
const { v, onSubmit } = useForm(form, fields)
|
||||
|
||||
function onQueriesResponse() {
|
||||
form.domain = mainDomain.value
|
||||
form.value.domain = mainDomain.value
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const data = await formatFormData(form, { flatten: true })
|
||||
const onUserCreate = onSubmit(async (onError) => {
|
||||
const data = await formatFormData(form.value, { flatten: true })
|
||||
api
|
||||
.post({ uri: 'users' }, data, {
|
||||
key: 'users.create',
|
||||
name: form.username,
|
||||
name: form.value.username,
|
||||
})
|
||||
.then(() => {
|
||||
router.push({ name: 'user-list' })
|
||||
})
|
||||
.catch((err: APIError) => {
|
||||
if (!(err instanceof APIBadRequestError)) throw err
|
||||
serverError.value = err.message
|
||||
})
|
||||
}
|
||||
.catch(onError)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ViewBase :loading="loading" skeleton="CardFormSkeleton">
|
||||
<CardForm
|
||||
:title="$t('users_new')"
|
||||
v-model="form"
|
||||
icon="user-plus"
|
||||
:validation="v$"
|
||||
:server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
:fields="fields"
|
||||
:title="$t('users_new')"
|
||||
:validations="v"
|
||||
@submit.prevent="onUserCreate"
|
||||
>
|
||||
<!-- USER NAME -->
|
||||
<FormField
|
||||
v-bind="fields.username"
|
||||
v-model="form.username"
|
||||
:validation="v$.form.username"
|
||||
/>
|
||||
|
||||
<!-- USER FULLNAME -->
|
||||
<FormField
|
||||
v-bind="fields.fullname"
|
||||
:validation="v$.form.fullname"
|
||||
v-model="form.fullname"
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<!-- USER MAIL DOMAIN -->
|
||||
<FormField v-bind="fields.domain" :validation="v$.form.domain">
|
||||
<template #default="{ self }">
|
||||
<template #component:domain="componentProps">
|
||||
<BInputGroup>
|
||||
<BInputGroupText id="local-part" tag="label" class="border-right-0">
|
||||
{{ form.username }}@
|
||||
</BInputGroupText>
|
||||
|
||||
<SelectItem
|
||||
aria-labelledby="local-part"
|
||||
aria-describedby="mail__BV_description_"
|
||||
v-model="form.domain"
|
||||
v-bind="self"
|
||||
/>
|
||||
<SelectItem v-bind="componentProps" v-model="form.domain" />
|
||||
</BInputGroup>
|
||||
</template>
|
||||
</FormField>
|
||||
<hr />
|
||||
|
||||
<!-- USER PASSWORD -->
|
||||
<FormField
|
||||
v-bind="fields.password"
|
||||
v-model="form.password"
|
||||
:validation="v$.form.password"
|
||||
/>
|
||||
|
||||
<!-- USER PASSWORD CONFIRMATION -->
|
||||
<FormField
|
||||
v-bind="fields.confirmation"
|
||||
v-model="form.confirmation"
|
||||
:validation="v$.form.confirmation"
|
||||
/>
|
||||
</CardForm>
|
||||
</ViewBase>
|
||||
</template>
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { computed, nextTick, reactive, ref } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import api from '@/api'
|
||||
import type ViewBase from '@/components/globals/ViewBase.vue'
|
||||
import { useArrayRule, useForm } from '@/composables/form'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { arrayDiff } from '@/helpers/commons'
|
||||
import { arrayDiff, asUnreffed } from '@/helpers/commons'
|
||||
import {
|
||||
emailForward,
|
||||
emailLocalPart,
|
||||
helpers,
|
||||
integer,
|
||||
minLength,
|
||||
minValue,
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
sizeToM,
|
||||
} from '@/helpers/yunohostArguments'
|
||||
import { useStoreGetters } from '@/store/utils'
|
||||
import type { AdressModelValue, FieldProps, FormFieldDict } from '@/types/form'
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
|
@ -44,124 +44,138 @@ const viewElem = ref<InstanceType<typeof ViewBase> | null>(null)
|
|||
|
||||
const { user, domainsAsChoices, mainDomain } = useStoreGetters()
|
||||
|
||||
const fields = {
|
||||
type Form = typeof form.value
|
||||
const form = ref({
|
||||
username: props.name,
|
||||
fullname: '',
|
||||
mail: { localPart: '', separator: '@', domain: '' } as AdressModelValue,
|
||||
mailbox_quota: '' as string | number,
|
||||
mail_aliases: [] as AdressModelValue[],
|
||||
mail_forward: [] as string[],
|
||||
change_password: '',
|
||||
confirmation: '',
|
||||
})
|
||||
const fields = reactive({
|
||||
username: {
|
||||
component: 'InputItem',
|
||||
label: t('user_username'),
|
||||
modelValue: props.name,
|
||||
props: { id: 'username', disabled: true },
|
||||
props: {
|
||||
id: 'username',
|
||||
disabled: true,
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['username']>,
|
||||
|
||||
fullname: {
|
||||
component: 'InputItem',
|
||||
label: t('user_fullname'),
|
||||
rules: { required, nameValidator },
|
||||
props: {
|
||||
id: 'fullname',
|
||||
placeholder: t('placeholder.fullname'),
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['fullname']>,
|
||||
|
||||
mail: {
|
||||
component: 'AdressItem',
|
||||
label: t('user_email'),
|
||||
props: { id: 'mail', choices: domainsAsChoices },
|
||||
rules: {
|
||||
localPart: { required, email: emailLocalPart },
|
||||
},
|
||||
props: { id: 'mail', choices: asUnreffed(domainsAsChoices) },
|
||||
} satisfies FieldProps<'AdressItem', Form['mail']>,
|
||||
|
||||
mailbox_quota: {
|
||||
append: 'M',
|
||||
component: 'InputItem',
|
||||
label: t('user_mailbox_quota'),
|
||||
description: t('mailbox_quota_description'),
|
||||
example: t('mailbox_quota_example'),
|
||||
// example: t('mailbox_quota_example'),
|
||||
rules: { integer, minValue: minValue(0) },
|
||||
props: {
|
||||
id: 'mailbox-quota',
|
||||
placeholder: t('mailbox_quota_placeholder'),
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['mailbox_quota']>,
|
||||
|
||||
mail_aliases: {
|
||||
component: 'AdressItem',
|
||||
rules: asUnreffed(
|
||||
useArrayRule(() => form.value.mail_aliases, {
|
||||
localPart: { required, email: emailLocalPart },
|
||||
}),
|
||||
),
|
||||
props: {
|
||||
placeholder: t('placeholder.username'),
|
||||
choices: domainsAsChoices,
|
||||
},
|
||||
choices: asUnreffed(domainsAsChoices),
|
||||
},
|
||||
} satisfies FieldProps<'AdressItem', Form['mail_aliases']>,
|
||||
|
||||
mail_forward: {
|
||||
component: 'InputItem',
|
||||
rules: asUnreffed(
|
||||
useArrayRule(() => form.value.mail_forward, { required, emailForward }),
|
||||
),
|
||||
props: {
|
||||
placeholder: t('user_new_forward'),
|
||||
type: 'email',
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['mail_forward']>,
|
||||
|
||||
change_password: {
|
||||
component: 'InputItem',
|
||||
label: t('password'),
|
||||
description: t('good_practices_about_user_password'),
|
||||
descriptionVariant: 'warning',
|
||||
rules: { passwordLenght: minLength(8) },
|
||||
props: {
|
||||
id: 'change_password',
|
||||
type: 'password',
|
||||
placeholder: '••••••••',
|
||||
autocomplete: 'new-password',
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'InputItem', Form['change_password']>,
|
||||
|
||||
confirmation: {
|
||||
component: 'InputItem',
|
||||
label: t('password_confirmation'),
|
||||
rules: asUnreffed(
|
||||
computed(() => ({ passwordMatch: sameAs(form.value.change_password) })),
|
||||
),
|
||||
props: {
|
||||
id: 'confirmation',
|
||||
type: 'password',
|
||||
placeholder: '••••••••',
|
||||
autocomplete: 'new-password',
|
||||
},
|
||||
},
|
||||
}
|
||||
const form = reactive({
|
||||
fullname: '',
|
||||
mail: { localPart: '', separator: '@', domain: '' },
|
||||
mailbox_quota: '',
|
||||
mail_aliases: [],
|
||||
mail_forward: [],
|
||||
change_password: '',
|
||||
confirmation: '',
|
||||
})
|
||||
const rules = computed(() => ({
|
||||
fullname: { required, nameValidator },
|
||||
mail: {
|
||||
localPart: { required, email: emailLocalPart },
|
||||
},
|
||||
mailbox_quota: { integer, minValue: minValue(0) },
|
||||
mail_aliases: {
|
||||
$each: helpers.forEach({
|
||||
localPart: { required, email: emailLocalPart },
|
||||
}),
|
||||
},
|
||||
mail_forward: {
|
||||
$each: helpers.forEach({
|
||||
mail: { required, emailForward },
|
||||
}),
|
||||
},
|
||||
change_password: { passwordLenght: minLength(8) },
|
||||
confirmation: { passwordMatch: sameAs(form.change_password) },
|
||||
}))
|
||||
const v$ = useVuelidate(rules, form)
|
||||
const serverError = ref('')
|
||||
} satisfies FieldProps<'InputItem', Form['confirmation']>,
|
||||
} satisfies FormFieldDict<Form>)
|
||||
|
||||
const { v, onSubmit } = useForm(form, fields)
|
||||
|
||||
function onQueriesResponse(user_: any) {
|
||||
form.fullname = user_.fullname
|
||||
form.mail = adressToFormValue(user_.mail)
|
||||
form.value.fullname = user_.fullname
|
||||
form.value.mail = adressToFormValue(user_.mail)
|
||||
if (user_['mail-aliases']) {
|
||||
form.mail_aliases = user_['mail-aliases'].map((mail) =>
|
||||
form.value.mail_aliases = user_['mail-aliases'].map((mail) =>
|
||||
adressToFormValue(mail),
|
||||
)
|
||||
}
|
||||
if (user_['mail-forward']) {
|
||||
form.mail_forward = user_['mail-forward'].map((mail) => ({ mail })) // Copy value
|
||||
form.value.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) {
|
||||
form.mailbox_quota = sizeToM(user_['mailbox-quota'].limit)
|
||||
form.value.mailbox_quota = sizeToM(user_['mailbox-quota'].limit)
|
||||
} else {
|
||||
form.mailbox_quota = ''
|
||||
form.value.mailbox_quota = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const formData = await formatFormData(form, { flatten: true })
|
||||
const onUserEdit = onSubmit(async (onError, serverErrors) => {
|
||||
const { data: formData } = await formatFormData(form.value, {
|
||||
flatten: true,
|
||||
extract: ['username'],
|
||||
})
|
||||
// FIXME not sure computed can be executed?
|
||||
const user_ = user.value(props.name)
|
||||
const data = {}
|
||||
|
@ -169,7 +183,7 @@ async function onSubmit() {
|
|||
formData.mailbox_quota = ''
|
||||
}
|
||||
|
||||
formData.mail_forward = formData.mail_forward?.map((v) => v.mail)
|
||||
// formData.mail_forward = formData.mail_forward?.map((v) => v.mail)
|
||||
|
||||
for (const key of ['mail_aliases', 'mail_forward']) {
|
||||
const dashedKey = key.replace('_', '-')
|
||||
|
@ -193,7 +207,7 @@ async function onSubmit() {
|
|||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
serverError.value = t('error_modify_something')
|
||||
serverErrors.global = [t('error_modify_something')]
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -205,132 +219,45 @@ async function onSubmit() {
|
|||
.then(() => {
|
||||
router.push({ name: 'user-info', param: { name: props.name } })
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name !== 'APIBadRequestError') throw err
|
||||
serverError.value = err.message
|
||||
})
|
||||
}
|
||||
|
||||
function addEmailField(type: 'aliases' | 'forward') {
|
||||
form['mail_' + type].push(
|
||||
type === 'aliases'
|
||||
? { localPart: '', separator: '@', domain: mainDomain.value }
|
||||
: { mail: '' },
|
||||
)
|
||||
// Focus last input after rendering update
|
||||
nextTick(() => {
|
||||
const inputs = viewElem.value!.$el.querySelectorAll(`#mail-${type} input`)
|
||||
inputs[inputs.length - 1].focus()
|
||||
})
|
||||
}
|
||||
|
||||
function removeEmailField(type: 'aliases' | 'forward', index: number) {
|
||||
form['mail_' + type].splice(index, 1)
|
||||
}
|
||||
.catch(onError)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ViewBase ref="viewElem" :loading="loading" skeleton="CardFormSkeleton">
|
||||
<CardForm
|
||||
:title="$t('user_username_edit', { name })"
|
||||
v-model="form"
|
||||
icon="user"
|
||||
:validation="v$"
|
||||
:server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
:fields="fields"
|
||||
:title="$t('user_username_edit', { name })"
|
||||
:validations="v"
|
||||
@submit.prevent="onUserEdit"
|
||||
>
|
||||
<!-- USERNAME (disabled) -->
|
||||
<FormField v-bind="fields.username" />
|
||||
|
||||
<!-- USER FULLNAME -->
|
||||
<FormField
|
||||
v-bind="fields.fullname"
|
||||
v-model="form.fullname"
|
||||
:validation="v$.form.fullname"
|
||||
<template #field:mail_aliases="fieldProps">
|
||||
<FormFieldMultiple
|
||||
v-bind="fieldProps"
|
||||
v-model="form.mail_aliases"
|
||||
:add-btn-text="t('user_emailaliases_add')"
|
||||
:default-value="
|
||||
() => ({
|
||||
localPart: '',
|
||||
separator: '@',
|
||||
domain: mainDomain,
|
||||
})
|
||||
"
|
||||
:validation="v.form.mail_aliases"
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- USER EMAIL -->
|
||||
<FormField v-bind="fields.mail" :validation="v$.form.mail">
|
||||
<template #default="{ self }">
|
||||
<AdressItem v-bind="self" v-model="form.mail" />
|
||||
</template>
|
||||
</FormField>
|
||||
|
||||
<!-- MAILBOX QUOTA -->
|
||||
<FormField
|
||||
v-bind="fields.mailbox_quota"
|
||||
:validation="v$.form.mailbox_quota"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<BInputGroup append="M">
|
||||
<InputItem v-bind="self" v-model="form.mailbox_quota" />
|
||||
</BInputGroup>
|
||||
<template #field:mail_forward="fieldProps">
|
||||
<FormFieldMultiple
|
||||
v-bind="fieldProps"
|
||||
v-model="form.mail_forward"
|
||||
:add-btn-text="t('user_emailforward_add')"
|
||||
:default-value="() => ''"
|
||||
:validation="v.form.mail_forward"
|
||||
/>
|
||||
</template>
|
||||
</FormField>
|
||||
<hr />
|
||||
|
||||
<!-- MAIL ALIASES -->
|
||||
<FormField :label="$t('user_emailaliases')" id="mail-aliases">
|
||||
<div v-for="(mail, i) in form.mail_aliases" :key="i" class="mail-list">
|
||||
<FormField
|
||||
v-bind="fields.mail_aliases"
|
||||
:id="'mail_aliases' + i"
|
||||
:validation="v$.form.mail_aliases"
|
||||
:validation-index="i"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<AdressItem v-bind="self" v-model="form.mail_aliases[i]" />
|
||||
</template>
|
||||
</FormField>
|
||||
|
||||
<BButton variant="danger" @click="removeEmailField('aliases', i)">
|
||||
<YIcon :title="$t('delete')" iname="trash-o" />
|
||||
<span class="visually-hidden">{{ $t('delete') }}</span>
|
||||
</BButton>
|
||||
</div>
|
||||
|
||||
<BButton variant="success" @click="addEmailField('aliases')">
|
||||
<YIcon iname="plus" /> {{ $t('user_emailaliases_add') }}
|
||||
</BButton>
|
||||
</FormField>
|
||||
|
||||
<!-- MAIL FORWARD -->
|
||||
<FormField :label="$t('user_emailforward')" id="mail-forward">
|
||||
<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].mail"
|
||||
:id="'mail-forward' + i"
|
||||
:validation="v$.form.mail_forward"
|
||||
:validation-index="i"
|
||||
/>
|
||||
|
||||
<BButton variant="danger" @click="removeEmailField('forward', i)">
|
||||
<YIcon :title="$t('delete')" iname="trash-o" />
|
||||
<span class="visually-hidden">{{ $t('delete') }}</span>
|
||||
</BButton>
|
||||
</div>
|
||||
|
||||
<BButton variant="success" @click="addEmailField('forward')">
|
||||
<YIcon iname="plus" /> {{ $t('user_emailforward_add') }}
|
||||
</BButton>
|
||||
</FormField>
|
||||
<hr />
|
||||
|
||||
<!-- USER PASSWORD -->
|
||||
<FormField
|
||||
v-bind="fields.change_password"
|
||||
v-model="form.change_password"
|
||||
:validation="v$.form.change_password"
|
||||
/>
|
||||
|
||||
<!-- USER PASSWORD CONFIRMATION -->
|
||||
<FormField
|
||||
v-bind="fields.confirmation"
|
||||
v-model="form.confirmation"
|
||||
:validation="v$.form.confirmation"
|
||||
/>
|
||||
</CardForm>
|
||||
</ViewBase>
|
||||
</template>
|
||||
|
|
|
@ -1,30 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
import api from '@/api'
|
||||
import { useForm } from '@/composables/form'
|
||||
import { useAutoModal } from '@/composables/useAutoModal'
|
||||
import { required } from '@/helpers/validators'
|
||||
import { formatFormData } from '@/helpers/yunohostArguments'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import type { FieldProps, FileModelValue, FormFieldDict } from '@/types/form'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const modalConfirm = useAutoModal()
|
||||
|
||||
const fields = {
|
||||
type Form = typeof form.value
|
||||
const form = ref({
|
||||
csvfile: { file: null } as FileModelValue,
|
||||
update: false,
|
||||
delete: false,
|
||||
})
|
||||
const fields = reactive({
|
||||
csvfile: {
|
||||
component: 'FileItem',
|
||||
label: t('users_import_csv_file'),
|
||||
description: t('users_import_csv_file_desc'),
|
||||
component: 'FileItem',
|
||||
rules: { file: required },
|
||||
props: {
|
||||
id: 'csvfile',
|
||||
accept: 'text/csv',
|
||||
placeholder: t('placeholder.file'),
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'FileItem', Form['csvfile']>,
|
||||
|
||||
update: {
|
||||
label: t('users_import_update'),
|
||||
|
@ -33,28 +42,22 @@ const fields = {
|
|||
props: {
|
||||
id: 'update',
|
||||
},
|
||||
},
|
||||
} satisfies FieldProps<'CheckboxItem', Form['update']>,
|
||||
|
||||
delete: {
|
||||
component: 'CheckboxItem',
|
||||
label: t('users_import_delete'),
|
||||
description: t('users_import_delete_desc'),
|
||||
component: 'CheckboxItem',
|
||||
props: {
|
||||
id: 'delete',
|
||||
},
|
||||
},
|
||||
}
|
||||
const form = reactive({
|
||||
csvfile: { file: null },
|
||||
update: false,
|
||||
delete: false,
|
||||
})
|
||||
const rules = computed(() => ({}))
|
||||
const v$ = useVuelidate(rules, form)
|
||||
const serverError = ref('')
|
||||
} satisfies FieldProps<'CheckboxItem', Form['delete']>,
|
||||
} satisfies FormFieldDict<Form>)
|
||||
|
||||
async function onSubmit() {
|
||||
if (form.delete) {
|
||||
const { v, onSubmit } = useForm(form, fields)
|
||||
|
||||
const onUserImport = onSubmit(async (onError) => {
|
||||
if (form.value.delete) {
|
||||
const confirmed = await modalConfirm(
|
||||
t('users_import_confirm_destructive'),
|
||||
{ okTitle: t('users_import_delete_others') },
|
||||
|
@ -62,7 +65,7 @@ async function onSubmit() {
|
|||
if (!confirmed) return
|
||||
}
|
||||
|
||||
const requestArgs = { ...form } as Partial<typeof form>
|
||||
const requestArgs = { ...form.value } as Partial<Form>
|
||||
if (!requestArgs.delete) delete requestArgs.delete
|
||||
if (!requestArgs.update) delete requestArgs.update
|
||||
const data = await formatFormData(requestArgs)
|
||||
|
@ -78,31 +81,17 @@ async function onSubmit() {
|
|||
])
|
||||
router.push({ name: 'user-list' })
|
||||
})
|
||||
.catch((error) => {
|
||||
serverError.value = error.message
|
||||
})
|
||||
}
|
||||
.catch(onError)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardForm
|
||||
:title="$t('users_import')"
|
||||
v-model="form"
|
||||
icon="user-plus"
|
||||
:validation="v$"
|
||||
:server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<!-- CSV FILE -->
|
||||
<FormField
|
||||
v-bind="fields.csvfile"
|
||||
v-model="form.csvfile"
|
||||
:validation="v$.form.csvfile"
|
||||
:fields="fields"
|
||||
:title="$t('users_import')"
|
||||
:validations="v"
|
||||
@submit.prevent="onUserImport"
|
||||
/>
|
||||
|
||||
<!-- UPDATE -->
|
||||
<FormField v-bind="fields.update" v-model="form.update" />
|
||||
|
||||
<!-- DELETE -->
|
||||
<FormField v-bind="fields.delete" v-model="form.delete" />
|
||||
</CardForm>
|
||||
</template>
|
||||
|
|
Loading…
Reference in a new issue