149 lines
5.2 KiB
Vue
149 lines
5.2 KiB
Vue
<script setup>
|
|
import { reactive, computed, watch } from 'vue'
|
|
import Modal from '@/components/ui/Modal.vue'
|
|
import Button from '@/components/ui/Button.vue'
|
|
import {
|
|
emptyUserForm,
|
|
ROLE_OPTIONS,
|
|
validateUserForm,
|
|
} from '@/composables/userForm'
|
|
|
|
const props = defineProps({
|
|
open: { type: Boolean, default: false },
|
|
title: { type: String, default: 'Edit User' },
|
|
mode: { type: String, default: 'edit', validator: (v) => ['create', 'edit'].includes(v) },
|
|
initialValues: { type: Object, default: () => emptyUserForm() },
|
|
submitting: { type: Boolean, default: false },
|
|
error: { type: String, default: '' },
|
|
})
|
|
|
|
const emit = defineEmits(['close', 'submit'])
|
|
|
|
const values = reactive(emptyUserForm())
|
|
const touched = reactive({
|
|
username: false,
|
|
password: false,
|
|
displayName: false,
|
|
role: false,
|
|
})
|
|
|
|
const validation = computed(() => validateUserForm(values, props.mode))
|
|
const fieldErrors = computed(() => validation.value.errors)
|
|
|
|
watch(() => props.open, (isOpen) => {
|
|
if (!isOpen) return
|
|
Object.assign(values, props.initialValues)
|
|
for (const key of Object.keys(touched)) touched[key] = false
|
|
}, { immediate: true })
|
|
|
|
function onBlur(key) {
|
|
touched[key] = true
|
|
}
|
|
|
|
function showError(key) {
|
|
return Boolean(touched[key] && fieldErrors.value[key])
|
|
}
|
|
|
|
function submit() {
|
|
for (const key of Object.keys(touched)) touched[key] = true
|
|
if (!validation.value.valid) return
|
|
emit('submit', validation.value.payload)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Modal :open="open" :title="title" @close="emit('close')">
|
|
<form class="space-y-4" @submit.prevent="submit">
|
|
<div v-if="mode === 'create'">
|
|
<label class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
Username
|
|
</label>
|
|
<input
|
|
v-model="values.username"
|
|
type="text"
|
|
autocomplete="off"
|
|
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
:class="showError('username') ? 'border-red-500' : 'border-gray-300'"
|
|
@blur="onBlur('username')"
|
|
>
|
|
<p v-if="showError('username')" class="mt-1 text-xs text-red-600 dark:text-red-400">
|
|
{{ fieldErrors.username }}
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="mode === 'create'">
|
|
<label class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
Password
|
|
</label>
|
|
<input
|
|
v-model="values.password"
|
|
type="password"
|
|
autocomplete="new-password"
|
|
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
:class="showError('password') ? 'border-red-500' : 'border-gray-300'"
|
|
@blur="onBlur('password')"
|
|
>
|
|
<p v-if="showError('password')" class="mt-1 text-xs text-red-600 dark:text-red-400">
|
|
{{ fieldErrors.password }}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
Display name
|
|
</label>
|
|
<input
|
|
v-model="values.displayName"
|
|
type="text"
|
|
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
:class="showError('displayName') ? 'border-red-500' : 'border-gray-300'"
|
|
@blur="onBlur('displayName')"
|
|
>
|
|
<p v-if="showError('displayName')" class="mt-1 text-xs text-red-600 dark:text-red-400">
|
|
{{ fieldErrors.displayName }}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
Role
|
|
</label>
|
|
<select
|
|
v-model="values.role"
|
|
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
:class="showError('role') ? 'border-red-500' : 'border-gray-300'"
|
|
@blur="onBlur('role')"
|
|
>
|
|
<option v-for="opt in ROLE_OPTIONS" :key="opt.value" :value="opt.value">
|
|
{{ opt.label }}
|
|
</option>
|
|
</select>
|
|
<p v-if="showError('role')" class="mt-1 text-xs text-red-600 dark:text-red-400">
|
|
{{ fieldErrors.role }}
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="mode === 'edit'" class="flex items-center gap-2">
|
|
<input
|
|
id="user-active"
|
|
v-model="values.isActive"
|
|
type="checkbox"
|
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
>
|
|
<label for="user-active" class="text-sm text-gray-700 dark:text-gray-300">
|
|
Account active
|
|
</label>
|
|
</div>
|
|
|
|
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
|
|
|
<div class="flex justify-end gap-2">
|
|
<Button type="button" variant="ghost" @click="emit('close')">Cancel</Button>
|
|
<Button type="submit" :disabled="submitting">
|
|
{{ submitting ? 'Saving…' : 'Save' }}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
</template>
|