370 lines
15 KiB
Vue
370 lines
15 KiB
Vue
<script setup lang="ts">
|
|
import { useForm } from '@inertiajs/vue3';
|
|
import { KeyRound, UserRound } from 'lucide-vue-next';
|
|
import { computed, ref } from 'vue';
|
|
import {
|
|
save,
|
|
store,
|
|
} from '@/actions/App/Http/Controllers/Admin/UserManagementController';
|
|
import FormFeedback from '@/components/FormFeedback.vue';
|
|
import InputError from '@/components/InputError.vue';
|
|
import PasswordInput from '@/components/PasswordInput.vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useFormProtection } from '@/composables/useFormProtection';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import type { UserModel } from '@/types/models/user-model';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
mode: 'create' | 'edit';
|
|
user?: UserModel | null;
|
|
}>(),
|
|
{ user: null },
|
|
);
|
|
|
|
const emit = defineEmits<{ cancel: [] }>();
|
|
const { t } = useLocalization();
|
|
const isEdit = computed(() => props.mode === 'edit');
|
|
const confirmationOpen = ref(false);
|
|
const form = useForm(
|
|
'ManagedUser:' + props.mode + ':' + (props.user?.id ?? 'new'),
|
|
{
|
|
name: props.user?.name ?? '',
|
|
email: props.user?.email ?? '',
|
|
password: '',
|
|
password_confirmation: '',
|
|
must_change_password_on_next_login:
|
|
props.user?.must_change_password_on_next_login ?? true,
|
|
},
|
|
);
|
|
const protection = useFormProtection(form);
|
|
|
|
function performSubmit(): void {
|
|
confirmationOpen.value = false;
|
|
protection.submit(
|
|
isEdit.value && props.user ? save(props.user.id) : store(),
|
|
{ preserveScroll: true },
|
|
);
|
|
}
|
|
|
|
function requestSubmit(): void {
|
|
if (isEdit.value) {
|
|
confirmationOpen.value = true;
|
|
return;
|
|
}
|
|
|
|
performSubmit();
|
|
}
|
|
|
|
function updateConfirmation(open: boolean): void {
|
|
if (!form.processing) {
|
|
confirmationOpen.value = open;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Card class="gap-0 overflow-hidden rounded-xl py-0 shadow-none">
|
|
<CardHeader class="gap-stack border-b p-5 sm:p-6">
|
|
<div class="flex items-start gap-3">
|
|
<span
|
|
class="bg-accent text-primary flex size-10 shrink-0 items-center justify-center rounded-md"
|
|
>
|
|
<UserRound class="size-5" aria-hidden="true" />
|
|
</span>
|
|
<div class="gap-stack grid min-w-0">
|
|
<h2 class="text-title">
|
|
{{ t('adminUsers.form.detailsTitle') }}
|
|
</h2>
|
|
<p class="text-muted-foreground">
|
|
{{
|
|
t(
|
|
isEdit
|
|
? 'adminUsers.form.editDescription'
|
|
: 'adminUsers.form.createDescription',
|
|
)
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent class="p-5 sm:p-6">
|
|
<form
|
|
class="gap-page grid min-w-0"
|
|
:aria-busy="form.processing"
|
|
@submit.prevent="requestSubmit"
|
|
>
|
|
<FormFeedback
|
|
:state="protection.state.value"
|
|
@retry="performSubmit"
|
|
/>
|
|
|
|
<fieldset
|
|
class="gap-column grid min-w-0"
|
|
:disabled="form.processing"
|
|
>
|
|
<legend class="sr-only">
|
|
{{ t('adminUsers.form.detailsTitle') }}
|
|
</legend>
|
|
|
|
<div class="grid gap-5 sm:grid-cols-2">
|
|
<div class="grid min-w-0 gap-2">
|
|
<Label for="managed-user-name">{{
|
|
t('adminUsers.form.name')
|
|
}}</Label>
|
|
<Input
|
|
id="managed-user-name"
|
|
v-model="form.name"
|
|
name="name"
|
|
required
|
|
autofocus
|
|
autocomplete="name"
|
|
:placeholder="
|
|
t('adminUsers.form.namePlaceholder')
|
|
"
|
|
:aria-invalid="!!form.errors.name"
|
|
aria-describedby="managed-user-name-error"
|
|
/>
|
|
<InputError
|
|
id="managed-user-name-error"
|
|
:message="form.errors.name"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid min-w-0 gap-2">
|
|
<Label for="managed-user-email">{{
|
|
t('adminUsers.form.email')
|
|
}}</Label>
|
|
<Input
|
|
id="managed-user-email"
|
|
v-model="form.email"
|
|
name="email"
|
|
type="email"
|
|
required
|
|
autocomplete="email"
|
|
:placeholder="
|
|
t('adminUsers.form.emailPlaceholder')
|
|
"
|
|
:aria-invalid="!!form.errors.email"
|
|
aria-describedby="managed-user-email-error"
|
|
/>
|
|
<InputError
|
|
id="managed-user-email-error"
|
|
:message="form.errors.email"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<section
|
|
aria-labelledby="managed-user-password-title"
|
|
class="gap-column grid min-w-0 border-t pt-6"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<KeyRound
|
|
class="text-muted-foreground mt-0.5 size-5 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<div class="gap-stack grid min-w-0">
|
|
<h3
|
|
id="managed-user-password-title"
|
|
class="font-semibold"
|
|
>
|
|
{{ t('adminUsers.form.passwordTitle') }}
|
|
</h3>
|
|
<p class="text-caption text-muted-foreground">
|
|
{{
|
|
t(
|
|
isEdit
|
|
? 'adminUsers.form.passwordEditHelp'
|
|
: 'adminUsers.form.passwordCreateHelp',
|
|
)
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid gap-5 sm:grid-cols-2">
|
|
<div class="grid min-w-0 gap-2">
|
|
<Label for="managed-user-password">{{
|
|
t(
|
|
isEdit
|
|
? 'adminUsers.form.newPassword'
|
|
: 'adminUsers.form.password',
|
|
)
|
|
}}</Label>
|
|
<PasswordInput
|
|
id="managed-user-password"
|
|
v-model="form.password"
|
|
name="password"
|
|
:required="!isEdit"
|
|
autocomplete="new-password"
|
|
:placeholder="
|
|
t(
|
|
isEdit
|
|
? 'adminUsers.form.passwordEditPlaceholder'
|
|
: 'adminUsers.form.passwordPlaceholder',
|
|
)
|
|
"
|
|
:aria-invalid="!!form.errors.password"
|
|
aria-describedby="managed-user-password-error"
|
|
/>
|
|
<InputError
|
|
id="managed-user-password-error"
|
|
:message="form.errors.password"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid min-w-0 gap-2">
|
|
<Label
|
|
for="managed-user-password-confirmation"
|
|
>{{
|
|
t('adminUsers.form.confirmPassword')
|
|
}}</Label
|
|
>
|
|
<PasswordInput
|
|
id="managed-user-password-confirmation"
|
|
v-model="form.password_confirmation"
|
|
name="password_confirmation"
|
|
:required="!isEdit"
|
|
autocomplete="new-password"
|
|
:placeholder="
|
|
t(
|
|
'adminUsers.form.confirmPasswordPlaceholder',
|
|
)
|
|
"
|
|
:aria-invalid="
|
|
!!form.errors.password_confirmation
|
|
"
|
|
aria-describedby="managed-user-password-confirmation-error"
|
|
/>
|
|
<InputError
|
|
id="managed-user-password-confirmation-error"
|
|
:message="form.errors.password_confirmation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="grid gap-2 border-t pt-6">
|
|
<Label
|
|
for="managed-user-force-password-change"
|
|
class="bg-muted/35 flex cursor-pointer items-start gap-3 rounded-lg border p-4"
|
|
>
|
|
<Checkbox
|
|
id="managed-user-force-password-change"
|
|
v-model="
|
|
form.must_change_password_on_next_login
|
|
"
|
|
name="must_change_password_on_next_login"
|
|
:invalid="
|
|
!!form.errors
|
|
.must_change_password_on_next_login
|
|
"
|
|
aria-describedby="managed-user-force-password-change-help managed-user-force-password-change-error"
|
|
/>
|
|
<span class="grid gap-1">
|
|
<span>{{
|
|
t('adminUsers.form.forcePasswordChange')
|
|
}}</span>
|
|
<span
|
|
id="managed-user-force-password-change-help"
|
|
class="text-caption text-muted-foreground font-normal"
|
|
>
|
|
{{
|
|
t(
|
|
'adminUsers.form.forcePasswordChangeHelp',
|
|
)
|
|
}}
|
|
</span>
|
|
</span>
|
|
</Label>
|
|
<InputError
|
|
id="managed-user-force-password-change-error"
|
|
:message="
|
|
form.errors.must_change_password_on_next_login
|
|
"
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div
|
|
class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
|
|
>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
@click="emit('cancel')"
|
|
>
|
|
{{ t('common.cancel') }}
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
:loading="form.processing"
|
|
class="sm:min-w-36"
|
|
data-test="managed-user-submit"
|
|
>
|
|
{{
|
|
t(
|
|
isEdit
|
|
? 'adminUsers.form.reviewChanges'
|
|
: 'adminUsers.form.createAction',
|
|
)
|
|
}}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog :open="confirmationOpen" @update:open="updateConfirmation">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{{
|
|
t('adminUsers.form.confirmTitle', {
|
|
name: form.name || props.user?.name || '',
|
|
})
|
|
}}</DialogTitle>
|
|
<DialogDescription>
|
|
{{ t('adminUsers.form.confirmDescription') }}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div
|
|
class="border-warning/35 bg-warning/8 text-body rounded-md border p-4"
|
|
>
|
|
{{ t('adminUsers.form.confirmWarning') }}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
:disabled="form.processing"
|
|
@click="confirmationOpen = false"
|
|
>
|
|
{{ t('common.cancel') }}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
:loading="form.processing"
|
|
data-test="confirm-managed-user-update"
|
|
@click="performSubmit"
|
|
>
|
|
{{ t('adminUsers.form.confirmAction') }}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|