Skip to content

Commit

Permalink
admin: Add basic user management page + dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
katajakasa committed Apr 11, 2024
1 parent 5de1b74 commit 5a7b363
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 1 deletion.
6 changes: 6 additions & 0 deletions admin/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ const primaryLinks: NavigationLinks = [
},
];
const secondaryLinks: NavigationLinks = [
{
title: t("App.nav.users"),
icon: "fas fa-users",
to: "users",
noEventId: true,
},
{
title: t("App.nav.logout"),
icon: "fas fa-right-from-bracket",
Expand Down
145 changes: 145 additions & 0 deletions admin/src/components/UserDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<template>
<BaseFormDialog
:title="t('UserDialog.title')"
:ok-text="t('General.save')"
ok-icon="fas fa-floppy-disk"
:width="1000"
@submit="submit"
ref="dialog"
>
<v-form>
<v-text-field
v-model="username.value.value"
:error-messages="username.errorMessage.value"
variant="outlined"
readonly
:label="t('UserDialog.labels.userName')"
/>
<v-text-field
v-model="date_joined.value.value"
:error-messages="date_joined.errorMessage.value"
variant="outlined"
readonly
:label="t('UserDialog.labels.dateJoined')"
/>
<v-text-field
v-model="email.value.value"
:error-messages="email.errorMessage.value"
variant="outlined"
:label="t('UserDialog.labels.email')"
/>
<v-text-field
v-model="first_name.value.value"
:error-messages="first_name.errorMessage.value"
variant="outlined"
:label="t('UserDialog.labels.firstName')"
/>
<v-text-field
v-model="last_name.value.value"
:error-messages="last_name.errorMessage.value"
variant="outlined"
:label="t('UserDialog.labels.lastName')"
/>
</v-form>
</BaseFormDialog>
</template>

<script setup lang="ts">
import { type GenericObject, useField, useForm } from "vee-validate";
import { type Ref, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useToast } from "vue-toastification";
import { date as YupDate, object as yupObject, string as yupString } from "yup";
import type { User } from "@/api";
import BaseFormDialog from "@/components/BaseFormDialog.vue";
import BaseDialog from "@/components/BaseInfoDialog.vue";
import { useAPI } from "@/services/api";
const dialog: Ref<InstanceType<typeof BaseDialog> | undefined> = ref();
const { t } = useI18n();
const api = useAPI();
const toast = useToast();
const existingId: Ref<number | undefined> = ref(0);
// Form validation
const validationSchema = yupObject({
first_name: yupString().min(0).max(150),
last_name: yupString().min(0).max(150),
date_joined: YupDate().required(),
username: yupString().required().min(1).max(150),
email: yupString().required().min(1).max(254),
});
const { handleSubmit, setTouched, resetForm, setValues } = useForm({ validationSchema });
const first_name = useField<string>("first_name");
const last_name = useField<string>("last_name");
const date_joined = useField<string>("date_joined");
const username = useField<string>("username");
const email = useField<string>("email");
const submit = handleSubmit(async (values) => {
let ok: boolean;
if (existingId.value !== undefined) {
ok = await editItem(existingId.value, values);
} else {
ok = await createItem(values);
}
if (ok) {
dialog.value?.setResult(true);
}
});
async function createItem(values: GenericObject) {
try {
await api.users.usersCreate({
first_name: values.first_name,
last_name: values.last_name,
email: values.email,
username: values.username,
});
toast.success(t("UserDialog.createSuccess"));
return true;
} catch (e) {
toast.error(t("UserDialog.createFailure"));
console.error(e);
}
return false;
}
async function editItem(itemId: number, values: GenericObject) {
try {
await api.users.usersPartialUpdate(itemId, {
first_name: values.first_name,
last_name: values.last_name,
email: values.email,
username: values.username,
});
toast.success(t("UserDialog.editSuccess"));
return true;
} catch (e) {
toast.error(t("UserDialog.editFailure"));
console.error(e);
}
return false;
}
async function modal(item: User | undefined = undefined) {
if (item !== undefined) {
existingId.value = item.id;
setValues({
first_name: item.first_name ?? "",
last_name: item.last_name ?? "",
email: item.email,
date_joined: item.date_joined,
username: item.username,
});
} else {
existingId.value = undefined;
resetForm();
setTouched(false);
}
return (await dialog.value?.modal()) ?? false;
}
defineExpose({ modal });
</script>
4 changes: 3 additions & 1 deletion admin/src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
faRightFromBracket,
faRightToBracket,
faSitemap,
faUsers,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import {
Expand Down Expand Up @@ -73,7 +74,8 @@ export function setupIcons(app: App): void {
faXmark,
faFloppyDisk,
faPenToSquare,
faCalendarDays
faCalendarDays,
faUsers
);
library.add(faGoogle, faSteam, faGithub);

Expand Down
34 changes: 34 additions & 0 deletions admin/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"site": "Site",
"blog": "Blog",
"events": "Events",
"users": "Users",
"logout": "Log out"
}
},
Expand All @@ -65,6 +66,25 @@
"deleteFailure": "Failed to delete event; try again later.",
"deleteSuccess": "Event deleted!"
},
"UsersView": {
"title": "Users",
"newUser": "New user",
"loadingUsers": "Loading users ...",
"noUsersFound": "No users found.",
"confirmDelete": "Do your really want to delete user '{name}' ?",
"headers": {
"id": "ID",
"userName": "Username",
"firstName": "First name",
"lastName": "Last name",
"email": "Email address",
"dateJoined": "Date joined",
"actions": "Actions"
},
"loadFailure": "Failed to load users; try again later.",
"deleteFailure": "Failed to delete user; try again later.",
"deleteSuccess": "User deleted!"
},
"BlogEditorView": {
"title": "Blog",
"newBlogPost": "New blog post",
Expand Down Expand Up @@ -98,6 +118,20 @@
"editSuccess": "Blog post edited!",
"createSuccess": "Blog post created!"
},
"UserDialog": {
"title": "User",
"labels": {
"userName": "Username",
"firstName": "First name",
"lastName": "Last name",
"email": "Email address",
"dateJoined": "Date joined"
},
"editFailure": "Failed to edit user; try again later.",
"createFailure": "Failed to create user; try again later.",
"editSuccess": "User edited!",
"createSuccess": "User created!"
},
"EventDialog": {
"title": "Event",
"labels": {
Expand Down
10 changes: 10 additions & 0 deletions admin/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ const router = createRouter({
props: true,
component: () => import("@/views/EventView.vue"),
},
{
path: "/users",
name: "users",
meta: {
requireAuth: true,
requireViewPermission: PermissionTarget.USER,
},
props: true,
component: () => import("@/views/UsersView.vue"),
},
{
path: "/:eventId(\\d+)/blog",
name: "blog",
Expand Down
1 change: 1 addition & 0 deletions admin/src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum PermissionTarget {
STORE_TRANSACTION = "storetransaction",
STORE_TRANSACTION_EVENT = "storetransactionevent",
TRANSACTION_ITEM = "transactionitem",
USER = "user",
}

export function useAuth() {
Expand Down
Loading

0 comments on commit 5a7b363

Please sign in to comment.