Skip to content
Snippets Groups Projects
AdminUsersView.vue 8.17 KiB
<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>