edit users

This commit is contained in:
2025-07-21 09:37:53 +02:00
parent 6e0880eb3d
commit 50e066f794
46 changed files with 1284 additions and 232 deletions

View File

@@ -81,9 +81,14 @@ const warrenCrumbs = computed<WarrenBreadcrumbData[]>(() => {
</script>
<template>
<Breadcrumb>
<BreadcrumbList>
<template v-if="store.current == null">
<Breadcrumb class="flex-nowrap overflow-hidden">
<BreadcrumbList class="flex-nowrap">
<template
v-if="
store.current == null ||
!route.path.startsWith('/warrens/files')
"
>
<template v-for="(crumb, i) in routeCrumbs" :key="i">
<BreadcrumbItem>
<NuxtLink
@@ -97,10 +102,7 @@ const warrenCrumbs = computed<WarrenBreadcrumbData[]>(() => {
</NuxtLink>
<BreadcrumbPage v-else>{{ crumb.name }}</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator
v-if="i < routeCrumbs.length - 1"
class="hidden md:block"
/>
<BreadcrumbSeparator v-if="i < routeCrumbs.length - 1" />
</template>
</template>
<template
@@ -118,10 +120,7 @@ const warrenCrumbs = computed<WarrenBreadcrumbData[]>(() => {
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbPage v-else>{{ crumb.name }}</BreadcrumbPage>
<BreadcrumbSeparator
v-if="i < warrenCrumbs.length - 1"
class="hidden md:block"
/>
<BreadcrumbSeparator v-if="i < warrenCrumbs.length - 1" />
</template>
</BreadcrumbList>
</Breadcrumb>

View File

@@ -2,50 +2,48 @@
import { Switch } from '@/components/ui/switch';
import { useForm } from 'vee-validate';
import { createUserSchema } from '~/lib/schemas/admin';
import { toTypedSchema } from '@vee-validate/zod';
import type z from 'zod';
import { createUser } from '~/lib/api/admin/createUser';
import { toTypedSchema } from '@vee-validate/yup';
const adminStore = useAdminStore();
const creating = ref(false);
function cancel() {
adminStore.closeCreateUserDialog();
form.resetForm();
}
const form = useForm({
validationSchema: toTypedSchema(createUserSchema),
});
const onSubmit = form.handleSubmit(
async (values: z.output<typeof createUserSchema>) => {
if (creating.value) {
return;
}
creating.value = true;
const result = await createUser(values);
creating.value = false;
if (result.success) {
adminStore.closeCreateUserDialog();
}
const onSubmit = form.handleSubmit(async (values) => {
if (creating.value) {
return;
}
);
creating.value = true;
const result = await createUser(values);
creating.value = false;
if (result.success) {
adminStore.closeCreateUserDialog();
}
});
</script>
<template>
<Dialog :open="adminStore.createUserDialog != null">
<DialogTrigger><slot /></DialogTrigger>
<DialogContent @escape-key-down="cancel">
<DialogHeader>
<DialogTitle>Create user</DialogTitle>
<DialogDescription>
<AlertDialog :open="adminStore.createUserDialogOpen">
<AlertDialogTrigger><slot /></AlertDialogTrigger>
<AlertDialogContent @escape-key-down="cancel">
<AlertDialogHeader>
<AlertDialogTitle>Create user</AlertDialogTitle>
<AlertDialogDescription>
Enter a username, email and password to create a new user
</DialogDescription>
</DialogHeader>
</AlertDialogDescription>
</AlertDialogHeader>
<form
id="create-user-form"
@@ -67,8 +65,8 @@ const onSubmit = form.handleSubmit(
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
<FormMessage />
</FormField>
<FormField v-slot="{ componentField }" name="email">
@@ -104,8 +102,8 @@ const onSubmit = form.handleSubmit(
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
<FormMessage />
</FormField>
<FormField v-slot="{ value, handleChange }" name="admin">
@@ -118,15 +116,19 @@ const onSubmit = form.handleSubmit(
@update:model-value="handleChange"
/>
</FormControl>
<FormMessage />
</FormItem>
<FormMessage />
</FormField>
</form>
<DialogFooter>
<Button variant="outline" @click="cancel">Cancel</Button>
<Button type="submit" form="create-user-form">Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialogFooter>
<AlertDialogCancel variant="outline" @click="cancel"
>Cancel</AlertDialogCancel
>
<AlertDialogAction type="submit" form="create-user-form"
>Create</AlertDialogAction
>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>

View File

@@ -35,7 +35,7 @@ adminStore.$subscribe(async (_mutation, state) => {
function close() {
confirmEmail.value = '';
adminStore.clearDeleteUserDialog();
adminStore.closeDeleteUserDialog();
}
async function submit() {

View File

@@ -0,0 +1,185 @@
<script setup lang="ts">
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/yup';
import { editUserSchema } from '~/lib/schemas/admin';
import { editUser } from '~/lib/api/admin/editUser';
import type { AuthUser } from '~/shared/types/auth';
const adminStore = useAdminStore();
const isValid = computed(() => Object.keys(form.errors.value).length < 1);
// We'll only update this value if there is a user to prevent layout shifts on close
const user = ref<AuthUser>();
const editing = ref(false);
const isChanged = computed(() => {
if (user.value == null) {
return false;
}
try {
const values = editUserSchema.validateSync(form.controlledValues.value);
return (
values.name !== user.value.name ||
values.email !== user.value.email ||
values.password != null ||
values.admin !== user.value.admin
);
} catch {
return true;
}
});
function close() {
adminStore.closeEditUserDialog();
}
const form = useForm({
validationSchema: toTypedSchema(editUserSchema),
});
adminStore.$subscribe((_mutation, state) => {
if (state.editUserDialog != null && !editing.value) {
user.value = state.editUserDialog.user;
form.setValues(user.value);
}
});
const onSubmit = form.handleSubmit(async (values) => {
if (user.value == null || !isChanged.value || editing.value) {
return;
}
editing.value = true;
const result = await editUser({
id: user.value.id,
name: values.name,
email: values.email,
password: values.password ?? null,
admin: values.admin,
});
if (result.success) {
close();
}
editing.value = false;
});
</script>
<template>
<AlertDialog :open="adminStore.editUserDialog != null">
<AlertDialogTrigger><slot /></AlertDialogTrigger>
<AlertDialogContent @escape-key-down="close">
<AlertDialogHeader>
<AlertDialogTitle>Edit user</AlertDialogTitle>
<AlertDialogDescription>
Edit the user's fields, manage permissions or assign warrens
</AlertDialogDescription>
</AlertDialogHeader>
<form
id="edit-user-form"
class="flex flex-col gap-2"
@submit.prevent="onSubmit"
>
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
v-bind="componentField"
id="username"
type="text"
placeholder="confused-cat"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
v-bind="componentField"
id="email"
type="email"
placeholder="confusedcat@example.com"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
v-bind="componentField"
id="password"
type="password"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</FormControl>
<FormMessage />
<FormDescription
>Leave empty to keep the current
password</FormDescription
>
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="admin">
<FormItem>
<FormLabel>Admin</FormLabel>
<FormControl>
<Switch
id="admin"
:model-value="value"
@update:model-value="handleChange"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</form>
<AlertDialogFooter class="gap-y-0">
<AlertDialogCancel @click="close">Cancel</AlertDialogCancel>
<AlertDialogAction
type="submit"
form="edit-user-form"
:disabled="!isChanged || !isValid"
>Save</AlertDialogAction
>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>

View File

@@ -11,15 +11,21 @@ const AVATAR =
</script>
<template>
<div class="group/user flex flex-row items-center justify-between gap-4">
<div
class="group/user bg-accent/30 flex cursor-pointer flex-row items-center justify-between gap-4 overflow-hidden rounded-lg p-2 pl-3"
>
<Avatar>
<AvatarImage :src="AVATAR" />
</Avatar>
<div class="flex grow flex-col leading-4">
<span class="text-sm font-medium">{{ user.name }}</span>
<span class="text-muted-foreground text-xs">{{ user.email }}</span>
<div class="flex min-w-0 shrink grow flex-col leading-4">
<span class="truncate text-sm font-medium">{{ user.name }}</span>
<span class="text-muted-foreground truncate text-xs">{{
user.email
}}</span>
</div>
<div class="opacity-0 transition-all group-hover/user:opacity-100">
<div
class="flex justify-end transition-all not-pointer-coarse:opacity-0 not-pointer-coarse:group-hover/user:opacity-100"
>
<slot name="actions" />
</div>
</div>