<script setup lang="ts"> import { useUserStore } from "@/stores/users"; import { onMounted, reactive } from "vue"; import { RoleEnum, type UserOutExtended } from "@/client"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import BootstrapToast from "@/components/BootstrapToast.vue"; import { Toast } from "bootstrap"; import CreateUserModal from "@/components/admin/CreateUserModal.vue"; const userRepository = useUserStore(); type RoleList = RoleEnum[]; let successRoleToast: Toast | undefined; let successInvitationToast: Toast | undefined; let errorToast: Toast | undefined; const userState = reactive<{ loading: boolean; users: UserOutExtended[]; newUserRoles: RoleList[]; editUserRoles: boolean[]; editedUser?: UserOutExtended; errorMessage: string; searchString: string; userRoles: RoleEnum[]; inspectUser?: UserOutExtended; searched: boolean; }>({ loading: false, users: [], newUserRoles: [], editUserRoles: [], editedUser: undefined, errorMessage: "", searchString: "", userRoles: [], inspectUser: undefined, searched: false, }); function searchUsers() { userState.loading = true; userRepository .fetchUsers( userState.searchString ? userState.searchString : undefined, userState.userRoles ? userState.userRoles : undefined, ) .then((users) => { userState.users = users; userState.newUserRoles = users.map((user) => user.roles); userState.editUserRoles = users.map(() => false); }) .finally(() => { userState.loading = false; userState.searched = true; }); } function saveUserRoles(index: number) { userState.loading = true; userState.editedUser = userState.users[index]; userRepository .updateUserRoles(userState.users[index].uid, userState.newUserRoles[index]) .then((user) => { userState.users[index] = user; userState.editUserRoles[index] = false; successRoleToast?.show(); }) .catch((err) => { userState.errorMessage = err.body?.detail; errorToast?.show(); }) .finally(() => { userState.loading = false; }); } function resetUserRoles(index: number) { userState.editUserRoles[index] = false; userState.newUserRoles[index] = JSON.parse( JSON.stringify(userState.users[index].roles), ); } function resendInvitationEmail(uid: string) { userState.loading = true; userRepository .resendInvitationEmail(uid) .then(() => { successInvitationToast?.show(); }) .finally(() => { userState.loading = false; }); } onMounted(() => { successRoleToast = new Toast("#change-role-success-toast"); successInvitationToast = new Toast("#resend-invitation-success-toast"); errorToast = new Toast("#change-role-error-toast"); }); </script> <template> <bootstrap-toast toast-id="resend-invitation-success-toast"> Successfully resend invitation email </bootstrap-toast> <bootstrap-toast toast-id="change-role-success-toast"> Successfully change roles of user {{ userState.editedUser?.display_name }} </bootstrap-toast> <bootstrap-toast toast-id="change-role-error-toast" color-class="danger"> <template #default >Couldn't change the roles of {{ userState.editedUser?.display_name }} </template> <template #body>Error: {{ userState.errorMessage }}</template> </bootstrap-toast> <create-user-modal modal-id="create-user-modal" /> <div class="row border-bottom mb-4 justify-content-between align-items-center pb-2 pe-2" > <h2 class="w-fit">Manage Users</h2> <button type="button" class="btn btn-primary w-fit" data-bs-toggle="modal" data-bs-target="#create-user-modal" > Invite User </button> </div> <form @submit.prevent="searchUsers" id="admin-user-search-form"> <div class="d-flex justify-content-evenly align-content-center"> <div class="mx-2"> <label for="admin-user-state-select" class="form-label" >User Roles</label > <select v-model="userState.userRoles" multiple class="form-select mb-4 w-fit" id="admin-user-state-select" > <option v-for="role in Object.values(RoleEnum)" :key="role"> {{ role }} </option> </select> </div> <div class="flex-fill mx-2"> <label for="admin-user-name-search" class="form-label" >Name of a user</label > <div class="input-group"> <div class="input-group-text"> <font-awesome-icon icon="fa-solid fa-search" /> </div> <input id="admin-user-name-search" type="text" class="form-control" maxlength="32" minlength="3" v-model="userState.searchString" placeholder="Search for user name" /> </div> </div> </div> <button type="submit" class="btn btn-primary w-fit" :disabled="userState.loading" > Search </button> </form> <table class="table table-striped align-middle caption-top" v-if="userState.users" > <caption> Displaying {{ userState.users.length }} Users </caption> <thead> <tr> <th scope="col"><b>Name</b></th> <th scope="col">UID</th> <th v-for="role in Object.values(RoleEnum)" :key="role" scope="col" class="text-center" > {{ role.toUpperCase() }} </th> <th scope="col"></th> </tr> </thead> <tbody v-if="userState.users.length === 0"> <tr> <td colspan="8" class="text-center fst-italic fw-light"> <template v-if="userState.searched" >No Users found with specified filters </template> <template v-else>Select a filter and search for Users</template> </td> </tr> </tbody> <tbody v-else> <tr v-for="(user, index) in userState.users" :key="user.uid"> <th scope="row">{{ user.display_name }}</th> <td>{{ user.uid }}</td> <td v-for="role in Object.values(RoleEnum)" :key="role" class="text-center" > <input type="checkbox" class="form-check-input" :value="role" :disabled="!userState.editUserRoles[index]" v-model="userState.newUserRoles[index]" /> </td> <td class="text-end" v-if="userState.editUserRoles[index]"> <div class="btn-group btn-group-sm" role="group" aria-label="Basic mixed styles example" > <button type="button" class="btn btn-success" :disabled="userState.loading" @click="saveUserRoles(index)" > Save </button> <button type="button" class="btn btn-danger" :disabled="userState.loading" @click="resetUserRoles(index)" > Cancel </button> </div> </td> <td class="text-end" v-else> <div class="btn-group"> <button type="button" class="btn btn-sm btn-secondary" @click="userState.editUserRoles[index] = true" > Edit Roles </button> <button v-if="user.invitation_token_created_at" type="button" class="btn btn-secondary btn-sm dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" > <span class="visually-hidden">Toggle Dropdown</span> </button> <ul class="dropdown-menu"> <li> <a class="dropdown-item" :class="{ disabled: userState.loading }" :aria-disabled="userState.loading" @click="resendInvitationEmail(user.uid)" >Resend invitation email</a > </li> </ul> </div> </td> </tr> </tbody> </table> </template> <style scoped></style>