Skip to content

Commit

Permalink
Release 2 (some additional fixes) (#148)
Browse files Browse the repository at this point in the history
* adminpanel tabel met mock data

* only require certificates when running during development

* quick fix

* run formatter

* finalize ui

* i18n

* use live data

* toggle admin

* toggle teacher

* sort isTeacher and isAdmin with true first

* Update frontend/src/components/project/submit/SubmitCard.vue

Co-authored-by: Pieter Janin <[email protected]>

* Update frontend/src/components/project/submit/SubmitCard.vue

Co-authored-by: Pieter Janin <[email protected]>

---------

Co-authored-by: Marieke <[email protected]>
Co-authored-by: Pieter Janin <[email protected]>
  • Loading branch information
3 people authored Apr 19, 2024
1 parent 861bbaf commit 28f4f9c
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 21 deletions.
15 changes: 12 additions & 3 deletions frontend/src/components/project/submit/SubmitCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
<v-card-item>
<v-card-title>{{ $t("submit.submit_title") }}</v-card-title>
</v-card-item>
<v-container class="card-container">
<h1 v-if="isError">Error</h1>
<v-container v-else class="card-container">
<v-row>
<v-col>
<ProjectMiniCard :projectId="projectId" />
<v-skeleton-loader :loading="isLoading" type="article">
<ProjectMiniCard :project="project!" />
</v-skeleton-loader>
</v-col>
<v-spacer />
<v-spacer />
Expand All @@ -30,10 +33,16 @@
<script setup lang="ts">
import ProjectMiniCard from "@/components/project/ProjectMiniCard.vue";
import SubmitForm from "@/components/project/submit/SubmitForm.vue";
import { useProjectQuery } from "@/queries/Project";
import { toRefs } from "vue";
defineProps<{
const props = defineProps<{
projectId: number;
}>();
const { projectId } = toRefs(props);
const { data: project, isLoading, isError } = useProjectQuery(projectId);
</script>

<style scoped>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ export default {
subjects: "My subjects",
announcements: "Announcements",
},
admin: {
users: "Users",
userTable: {
name: "Name",
uid: "UGent ID",
email: "Email",
isTeacher: "Is Teacher",
isAdmin: "Is Admin",
},
},
subject: {
register: "Register to subject:",
academy_year: "Academic year",
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/i18n/locales/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,16 @@ export default {
subjects: "Mijn vakken",
announcements: "Meldingen",
},

admin: {
users: "Gebruikers",
userTable: {
name: "Naam",
uid: "UGent ID",
email: "Email",
isTeacher: "Is Lesgever",
isAdmin: "Is Beheerder",
},
},
subject: {
register: "Registreer bij vak:",
academy_year: "Academiejaar",
Expand Down
71 changes: 59 additions & 12 deletions frontend/src/queries/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import {
type UseMutationReturnType,
} from "@tanstack/vue-query";
import type User from "@/models/User";
import { getMySubjects, getUser, toggleAdmin } from "@/services/user";
import { getMySubjects, getUser, getUsers, toggleAdmin, toggleTeacher } from "@/services/user";
import { type Ref, computed } from "vue";
import type { UserSubjectList } from "@/models/Subject";

function USER_QUERY_KEY(uid: string | null): string[] {
return uid ? ["user", uid] : ["user"];
}

function USERS_QUERY_KEY(): string[] {
return ["users"];
}

export function useUserQuery(uid: Ref<string | undefined> | null): UseQueryReturnType<User, Error> {
return useQuery<User, Error>({
queryKey: computed(() => USER_QUERY_KEY(uid?.value!)),
Expand All @@ -22,27 +26,70 @@ export function useUserQuery(uid: Ref<string | undefined> | null): UseQueryRetur
});
}

// TODO: Now only toggles current user
export function useToggleAdminMutation(): UseMutationReturnType<void, Error, User, void> {
export function useUsersQuery(): UseQueryReturnType<User[], Error> {
return useQuery<User[], Error>({
queryKey: USERS_QUERY_KEY(),
queryFn: () => getUsers(),
});
}

// TODO: Use USER_QUERY_KEY(uid) instead of USERS_QUERY_KEY() for invalidation
function useToggleMutation(
toggleFn: (uid: string) => Promise<User>,
getField: (_: User) => boolean,
setField: (_: User, value: boolean) => void
): UseMutationReturnType<User, Error, string, { previousUsers: User[] }> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleAdmin,
onMutate: async (user: User) => {
await queryClient.cancelQueries({ queryKey: USER_QUERY_KEY(null) });
queryClient.setQueryData<User>(USER_QUERY_KEY(null), () => {
return { ...user, is_admin: !user.is_admin };
mutationFn: async (uid) => await toggleFn(uid),
onMutate: async (uid: string) => {
const users = queryClient.getQueryData<User[]>(USERS_QUERY_KEY());
await queryClient.cancelQueries({ queryKey: USERS_QUERY_KEY() });
queryClient.setQueryData<User[]>(USERS_QUERY_KEY(), () => {
return users?.map((user: User) => {
const mappedUser = { ...user };
setField(mappedUser, user.uid === uid ? !getField(user) : getField(user));
return mappedUser;
});
});
return { previousUsers: users! };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY(null) });
onSettled: (_, __, uid, ctx) => {

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'_' is defined but never used

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'__' is defined but never used

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'uid' is defined but never used

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'ctx' is defined but never used
queryClient.invalidateQueries({ queryKey: USERS_QUERY_KEY() });
},
onError: (_, user) => {
onError: (_, uid, ctx) => {
queryClient.setQueryData<User[]>(USERS_QUERY_KEY(), () => ctx!.previousUsers!);
alert("Could not update user");
queryClient.setQueryData<User>(USER_QUERY_KEY(null), () => user!);
},
});
}

export function useToggleAdminMutation(): UseMutationReturnType<
User,
Error,
string,
{ previousUsers: User[] }
> {
return useToggleMutation(
toggleAdmin,
(user) => user.is_admin,
(user, value) => (user.is_admin = value)
);
}

export function useToggleTeacherMutation(): UseMutationReturnType<
User,
Error,
string,
{ previousUsers: User[] }
> {
return useToggleMutation(
toggleTeacher,
(user) => user.is_teacher,
(user, value) => (user.is_teacher = value)
);
}

// Hook for fetching subjects for a user
export function useMySubjectsQuery(): UseQueryReturnType<UserSubjectList, Error> {
return useQuery<UserSubjectList, Error>({
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ export async function getUser(uid?: string): Promise<User> {
return authorized_fetch(`/api/users/${uid || "me"}`, { method: "GET" });
}

export async function toggleAdmin() {
return authorized_fetch<void>("/api/users/me", { method: "POST" });
export async function getUsers(): Promise<User[]> {
return authorized_fetch("/api/users", { method: "GET" });
}

export async function toggleAdmin(uid: string): Promise<User> {
return authorized_fetch<User>(`/api/users/${uid}/admin`, { method: "POST" });
}

export async function toggleTeacher(uid: string): Promise<User> {
return authorized_fetch<User>(`/api/users/${uid}/teacher`, { method: "POST" });
}

// Fetches all subjects for logged in user
Expand Down
126 changes: 125 additions & 1 deletion frontend/src/views/AdminView.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,127 @@
<template>
<h1>Admin Panel</h1>
<div class="adminpanel">
<v-card :title="$t('admin.users')" flat class="bg-white">
<v-card-title>
<v-text-field
prepend-inner-icon="mdi-magnify"
v-model="search"
label="Search"
single-line
hide-details
></v-text-field>
</v-card-title>
<v-data-table-virtual
:headers="headers"
:items="users"
:search="search"
v-model:sortBy="sortBy"
item-value="uid"
:loading="isUserLoading || isUsersLoading"
density="compact"
class="table"
>
<template v-slot:loading>
<v-skeleton-loader type="table-row@15" class="table"></v-skeleton-loader>
</template>
<template v-slot:[`item.is_teacher`]="{ item }">
<v-checkbox-btn
:model-value="item.is_teacher"
:disabled="item.uid === currentUser?.uid"
@update:model-value="() => onToggleTeacher(item)"
></v-checkbox-btn>
</template>
<template v-slot:[`item.is_admin`]="{ item }">
<v-checkbox-btn
:model-value="item.is_admin"
:disabled="item.uid === currentUser?.uid"
@update:model-value="() => onToggleAdmin(item)"
></v-checkbox-btn>
</template>
</v-data-table-virtual>
</v-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import {
useUserQuery,
useUsersQuery,
useToggleAdminMutation,
useToggleTeacherMutation,
} from "@/queries/User";
import type User from "@/models/User";
const { t } = useI18n();
const { data: currentUser, isLoading: isUserLoading } = useUserQuery(null);
const { data: users, isLoading: isUsersLoading } = useUsersQuery();
const { mutateAsync: toggleAdmin } = useToggleAdminMutation();
const { mutateAsync: toggleTeacher } = useToggleTeacherMutation();
/**
* Sorts boolean values in descending order.
*/
function sortBool(a: boolean, b: boolean): number {
return a === b ? 0 : a ? -1 : 1;
}
const search = ref("");
const sortBy = ref([{ key: "given_name", order: "asc" }]);
async function onToggleAdmin(user: User) {
await toggleAdmin(user.uid);
}
async function onToggleTeacher(user: User) {
await toggleTeacher(user.uid);
}
const headers = ref([
{
title: computed(() => t("admin.userTable.name")),
key: "given_name",
align: "start",
sortable: true,
},
{
title: computed(() => t("admin.userTable.uid")),
key: "uid",
align: "start",
sortable: false,
},
{
title: computed(() => t("admin.userTable.email")),
key: "mail",
align: "start",
sortable: false,
},
{
title: computed(() => t("admin.userTable.isTeacher")),
key: "is_teacher",
sortable: true,
filterable: false,
filter: () => true, // disable filter
sort: sortBool,
},
{
title: computed(() => t("admin.userTable.isAdmin")),
key: "is_admin",
sortable: true,
filterable: false,
filter: () => true, // disable filter
sort: sortBool,
},
]);
</script>
<style scoped>
.adminpanel {
margin: 15px;
}
.table {
color: black !important;
background-color: white;
width: 95%;
}
</style>
4 changes: 2 additions & 2 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import vue from "@vitejs/plugin-vue";
import fs from "fs";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";

const isTesting = process.env.NODE_ENV === 'test'
const needsCertificate = process.env.NODE_ENV === 'development'

export default defineConfig({
plugins: [
Expand All @@ -22,7 +22,7 @@ export default defineConfig({
},
},
server: {
https: isTesting ? undefined : {
https: !needsCertificate ? undefined : {
key: fs.readFileSync('./local-cert/localhost-key.pem'),
cert: fs.readFileSync('./local-cert/localhost.pem')
},
Expand Down

0 comments on commit 28f4f9c

Please sign in to comment.