feature: Degraded Operations Visibility
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user