mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
9.6 KiB
302 lines
9.6 KiB
2 years ago
|
<script setup lang="ts">
|
||
2 years ago
|
import KebabIcon from '~icons/ic/baseline-more-vert'
|
||
2 years ago
|
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
|
||
2 years ago
|
import UsersModal from './user-management/UsersModal.vue'
|
||
2 years ago
|
import { projectRoleTagColors } from '~/utils/userUtils'
|
||
|
import MidAccountIcon from '~icons/mdi/account-outline'
|
||
|
import ReloadIcon from "~icons/mdi/reload"
|
||
|
import MdiEditIcon from '~icons/ic/round-edit'
|
||
|
import SearchIcon from '~icons/ic/round-search'
|
||
2 years ago
|
import { useClipboard } from '@vueuse/core'
|
||
2 years ago
|
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
|
||
|
import EmailIcon from '~icons/eva/email-outline'
|
||
|
import MdiPlusIcon from '~icons/mdi/plus'
|
||
|
import MdiContentCopyIcon from '~icons/mdi/content-copy'
|
||
|
import MdiEmailSendIcon from '~icons/mdi/email-arrow-right-outline'
|
||
|
import RolesIcon from '~icons/mdi/drama-masks'
|
||
2 years ago
|
import { User } from '~/lib/types'
|
||
|
import { watchDebounced } from '@vueuse/core'
|
||
2 years ago
|
import { useToast } from 'vue-toastification'
|
||
2 years ago
|
import FeedbackForm from './user-management/FeedbackForm.vue'
|
||
2 years ago
|
const toast = useToast()
|
||
|
|
||
|
const { $api, $e } = useNuxtApp()
|
||
|
const { project } = useProject()
|
||
2 years ago
|
const { copy } = useClipboard()
|
||
2 years ago
|
|
||
2 years ago
|
let users = $ref<null | User[]>(null)
|
||
2 years ago
|
let selectedUser = $ref<null | User>(null)
|
||
|
let showUserModal = $ref(false)
|
||
|
let showUserDeleteModal = $ref(false)
|
||
2 years ago
|
let isLoading = $ref(false)
|
||
2 years ago
|
|
||
|
let totalRows = $ref(0)
|
||
|
const currentPage = $ref(1)
|
||
|
const currentLimit = $ref(10)
|
||
|
const searchText = ref<string>('')
|
||
|
|
||
|
const loadUsers = async (page = currentPage, limit = currentLimit) => {
|
||
|
try {
|
||
|
if (!project.value?.id) return
|
||
|
|
||
2 years ago
|
// TODO: Types of api is not correct
|
||
2 years ago
|
const response = await $api.auth.projectUserList(project.value?.id, <any> {
|
||
|
query: {
|
||
|
limit,
|
||
|
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
|
||
|
query: searchText.value,
|
||
|
},
|
||
|
})
|
||
|
if (!response.users) return
|
||
|
|
||
|
totalRows = response.users.pageInfo.totalRows ?? 0
|
||
2 years ago
|
users = response.users.list as User[]
|
||
|
} catch (e: any) {
|
||
|
console.error(e)
|
||
|
toast.error(await extractSdkResponseErrorMsg(e))
|
||
2 years ago
|
}
|
||
|
}
|
||
|
|
||
2 years ago
|
const inviteUser = async (user: User) => {
|
||
2 years ago
|
try {
|
||
|
if (!project.value?.id) return
|
||
|
|
||
2 years ago
|
await $api.auth.projectUserAdd(project.value.id, user);
|
||
2 years ago
|
toast.success('Successfully added user to project');
|
||
|
await loadUsers();
|
||
2 years ago
|
} catch (e: any) {
|
||
|
console.error(e)
|
||
|
toast.error(await extractSdkResponseErrorMsg(e))
|
||
2 years ago
|
}
|
||
|
|
||
|
$e('a:user:add');
|
||
|
}
|
||
|
|
||
|
const deleteUser = async () => {
|
||
|
try {
|
||
|
if (!project.value?.id || !selectedUser?.id) return
|
||
|
|
||
|
await $api.auth.projectUserRemove(project.value.id, selectedUser.id);
|
||
|
toast.success('Successfully deleted user from project');
|
||
|
await loadUsers();
|
||
2 years ago
|
showUserDeleteModal = false;
|
||
2 years ago
|
} catch (e: any) {
|
||
|
console.error(e)
|
||
|
toast.error(await extractSdkResponseErrorMsg(e))
|
||
2 years ago
|
}
|
||
|
|
||
|
$e('a:user:delete');
|
||
|
}
|
||
|
|
||
|
const onEdit = (user: User) => {
|
||
|
selectedUser = user
|
||
|
showUserModal = true
|
||
|
}
|
||
|
|
||
|
const onInvite = () => {
|
||
|
selectedUser = null
|
||
|
showUserModal = true
|
||
|
}
|
||
|
|
||
|
const onDelete = (user: User) => {
|
||
|
selectedUser = user
|
||
|
showUserDeleteModal = true
|
||
|
}
|
||
|
|
||
|
const resendInvite = async (user: User) => {
|
||
|
if (!project.value?.id ) return
|
||
|
|
||
|
try {
|
||
|
await $api.auth.projectUserResendInvite(project.value.id, user.id);
|
||
|
toast.success('Invite email sent successfully');
|
||
|
await loadUsers();
|
||
2 years ago
|
} catch (e: any) {
|
||
|
console.error(e)
|
||
|
toast.error(await extractSdkResponseErrorMsg(e))
|
||
2 years ago
|
}
|
||
|
|
||
|
$e('a:user:resend-invite');
|
||
|
}
|
||
|
|
||
|
const copyInviteUrl = (user: User) => {
|
||
|
if(!user.invite_token) return
|
||
|
|
||
|
const getInviteUrl = (token: string) => `${location.origin}${location.pathname}#/user/authentication/signup/${token}`;
|
||
|
|
||
2 years ago
|
copy(getInviteUrl(user.invite_token) );
|
||
2 years ago
|
toast.success('Invite url copied to clipboard');
|
||
|
}
|
||
|
|
||
|
onMounted(async () => {
|
||
|
if (!users) {
|
||
2 years ago
|
isLoading = true
|
||
|
try {
|
||
|
await loadUsers()
|
||
|
} finally {
|
||
|
isLoading = false
|
||
|
}
|
||
2 years ago
|
}
|
||
|
})
|
||
|
|
||
2 years ago
|
watchDebounced(
|
||
|
searchText,
|
||
2 years ago
|
() => loadUsers(),
|
||
2 years ago
|
{ debounce: 300, maxWait: 600 },
|
||
2 years ago
|
)
|
||
2 years ago
|
|
||
2 years ago
|
</script>
|
||
|
|
||
|
<template>
|
||
2 years ago
|
<div v-if="isLoading" class="h-full w-full flex flex-row justify-center mt-42">
|
||
|
<a-spin size="large"/>
|
||
|
</div>
|
||
|
<div v-else class="flex flex-col w-full px-6">
|
||
2 years ago
|
<UsersModal :key="showUserModal" :show="showUserModal" :selected-user="selectedUser" @closed="showUserModal = false" @reload="loadUsers()"/>
|
||
2 years ago
|
<a-modal v-model:visible="showUserDeleteModal" :closable="false" width="28rem" centered :footer="null">
|
||
|
<div class="flex flex-col h-full">
|
||
|
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">
|
||
|
This action will remove this user from this project
|
||
|
</div>
|
||
|
<div class="flex mt-6 justify-center space-x-2">
|
||
|
<a-button @click="showUserDeleteModal = false"> Cancel </a-button>
|
||
|
<a-button type="primary" danger @click="deleteUser"> Confirm </a-button>
|
||
|
</div>
|
||
2 years ago
|
</div>
|
||
2 years ago
|
</a-modal>
|
||
2 years ago
|
<div class="flex flex-row mb-4 mx-4 justify-between">
|
||
|
<div class="flex w-1/3" >
|
||
|
<a-input v-model:value="searchText" placeholder="Filter by email" >
|
||
|
<template #prefix>
|
||
|
<SearchIcon class="text-gray-400"/>
|
||
|
</template>
|
||
|
</a-input>
|
||
|
</div>
|
||
|
|
||
|
<div class="flex flex-row space-x-1">
|
||
|
<a-button size="middle" type="text" @click="loadUsers()">
|
||
|
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
|
||
|
<ReloadIcon class="text-gray-500" />
|
||
|
<div class="text-gray-500">Reload</div>
|
||
|
</div>
|
||
|
</a-button>
|
||
|
<a-button size="middle" @click="onInvite">
|
||
|
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
|
||
|
<MidAccountIcon />
|
||
|
<div>Invite Team</div>
|
||
|
</div>
|
||
|
</a-button>
|
||
|
</div>
|
||
|
</div>
|
||
2 years ago
|
<div class="px-5">
|
||
2 years ago
|
<div class="flex flex-row border-b-1 pb-2 px-2">
|
||
|
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
|
||
|
<EmailIcon class="flex text-gray-500 -mt-0.5" />
|
||
|
|
||
|
<div class="text-gray-600 text-xs space-x-1">E-mail</div>
|
||
|
</div>
|
||
|
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1">
|
||
|
<RolesIcon class="flex text-gray-500 -mt-0.5" />
|
||
|
|
||
|
<div class="text-gray-600 text-xs">Role</div>
|
||
|
</div>
|
||
|
<div class="flex flex-row w-1/6 justify-end items-center pl-1">
|
||
|
<div class="text-gray-600 text-xs">Actions</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div v-for="(user, index) in users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
|
||
|
<div class="flex w-4/6 flex-wrap">
|
||
|
{{ user.email }}
|
||
|
</div>
|
||
|
<div class="flex w-1/6 justify-center flex-wrap ml-4">
|
||
2 years ago
|
<div :class="`rounded-full px-2 py-1 bg-[${projectRoleTagColors[user.roles]}]`">
|
||
2 years ago
|
{{ user.roles }}
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="flex w-1/6 flex-wrap justify-end">
|
||
2 years ago
|
<a-tooltip v-if="user.project_id" placement="bottom">
|
||
|
<template #title>
|
||
|
<span>Edit user</span>
|
||
2 years ago
|
</template>
|
||
2 years ago
|
<a-button type="text" class="!rounded-md" @click="onEdit(user)">
|
||
|
<template #icon>
|
||
2 years ago
|
<MdiEditIcon class="flex mx-auto h-[1rem]" />
|
||
2 years ago
|
</template>
|
||
|
</a-button>
|
||
|
</a-tooltip>
|
||
|
<a-tooltip v-if="!user.project_id" placement="bottom">
|
||
|
<template #title>
|
||
|
<span>Add user to the project</span>
|
||
2 years ago
|
</template>
|
||
2 years ago
|
<a-button type="text" class="!rounded-md " @click="inviteUser(user)">
|
||
|
<template #icon>
|
||
2 years ago
|
<MdiPlusIcon class="flex mx-auto h-[1.1rem]" />
|
||
2 years ago
|
</template>
|
||
|
</a-button>
|
||
|
</a-tooltip>
|
||
|
|
||
|
<a-tooltip v-else placement="bottom">
|
||
|
<template #title>
|
||
|
<span>Remove user from the project</span>
|
||
2 years ago
|
</template>
|
||
2 years ago
|
<a-button type="text" class="!rounded-md" @click="onDelete(user)">
|
||
|
<template #icon>
|
||
2 years ago
|
<MdiDeleteOutlineIcon class="flex mx-auto h-[1.1rem]" />
|
||
2 years ago
|
</template>
|
||
|
</a-button>
|
||
|
</a-tooltip>
|
||
|
|
||
2 years ago
|
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight">
|
||
2 years ago
|
<div class="flex flex-row items-center">
|
||
|
<a-button type="text" class="!px-0">
|
||
2 years ago
|
<div class="flex flex-row items-center h-[1.2rem]">
|
||
|
<KebabIcon />
|
||
2 years ago
|
</div>
|
||
|
</a-button>
|
||
|
</div>
|
||
|
<template #overlay>
|
||
|
<a-menu>
|
||
|
<a-menu-item>
|
||
2 years ago
|
<div class="flex flex-row items-center py-1" @click="resendInvite(user)">
|
||
2 years ago
|
<MdiEmailSendIcon class="flex h-[1rem]" />
|
||
2 years ago
|
<div class="text-xs pl-2">
|
||
|
Resend invite email
|
||
2 years ago
|
</div>
|
||
2 years ago
|
</div>
|
||
2 years ago
|
</a-menu-item>
|
||
|
<a-menu-item>
|
||
2 years ago
|
<div class="flex flex-row items-center py-1" @click="copyInviteUrl(user)">
|
||
2 years ago
|
<MdiContentCopyIcon class="flex h-[1rem]" />
|
||
2 years ago
|
<div class="text-xs pl-2">
|
||
|
Copy invite URL
|
||
2 years ago
|
</div>
|
||
2 years ago
|
</div>
|
||
2 years ago
|
</a-menu-item>
|
||
|
</a-menu>
|
||
2 years ago
|
</template>
|
||
2 years ago
|
</a-dropdown>
|
||
2 years ago
|
</div>
|
||
|
</div>
|
||
|
<a-pagination
|
||
|
hideOnSinglePage
|
||
|
v-model:current="currentPage"
|
||
|
class="mt-4"
|
||
|
:page-size="currentLimit"
|
||
|
:total="totalRows"
|
||
|
show-less-items
|
||
|
@change="loadUsers"
|
||
|
/>
|
||
2 years ago
|
<FeedbackForm />
|
||
2 years ago
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<style scoped>
|
||
|
.users-table {
|
||
|
/* equally spaced columns in table */
|
||
|
table-layout: fixed;
|
||
|
|
||
|
width: 100%;
|
||
|
}
|
||
|
</style>
|