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