Browse Source

feat(gui): integrate invite user

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4134/head
Pranav C 2 years ago
parent
commit
7d8ee2f29e
  1. 250
      packages/nc-gui/components/org-user/UsersModal.vue
  2. 108
      packages/nc-gui/components/org-user/index.vue
  3. 1
      packages/nc-gui/lang/en.json

250
packages/nc-gui/components/org-user/UsersModal.vue

@ -0,0 +1,250 @@
<script setup lang="ts">
import {
computed,
extractSdkResponseErrorMsg,
Form,
isEmail,
message,
onMounted,
projectRoles,
projectRoleTagColors,
ref,
useCopy,
useDashboard,
useI18n,
useNuxtApp,
} from '#imports'
import { UserType } from 'nocodb-sdk'
import type { User } from '~/lib'
import { ProjectRole, Role } from '~/lib'
interface Props {
show: boolean
selectedUser?: User
}
interface Users {
emails: string
role: Role.OrgLevelCreator | Role.OrgLevelViewer
invitationToken?: string
}
const { show } = defineProps<Props>()
const emit = defineEmits(['closed', 'reload'])
const { t } = useI18n()
const { $api, $e } = useNuxtApp()
const { copy } = useCopy()
const { dashboardUrl } = $(useDashboard())
const usersData = $ref<Users>({ emails: '', role: Role.OrgLevelViewer, invitationToken: undefined })
const formRef = ref()
const useForm = Form.useForm
const validators = computed(() => {
return {
emails: [
{
validator: (rule: any, value: string, callback: (errMsg?: string) => void) => {
if (!value || value.length === 0) {
callback('Email is required')
return
}
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !isEmail(e))
if (invalidEmails.length > 0) {
callback(`${invalidEmails.length > 1 ? ' Invalid emails:' : 'Invalid email:'} ${invalidEmails.join(', ')} `)
} else {
callback()
}
},
},
],
}
})
const { validateInfos } = useForm(usersData, validators)
const saveUser = async () => {
$e('a:org-user:invite', { role: usersData.role })
await formRef.value?.validateFields()
try {
// todo: update sdk(swagger.json)
const res = await $api.orgUsers.add({
roles: usersData.role,
email: usersData.emails,
} as unknown as UserType)
usersData.invitationToken = res.invite_token
emit('reload')
// Successfully updated the user details
message.success(t('msg.success.userDetailsUpdated'))
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
}
}
const inviteUrl = $computed(() => (usersData.invitationToken ? `${dashboardUrl}#/signup/${usersData.invitationToken}` : null))
const copyUrl = async () => {
if (!inviteUrl) return
await copy(inviteUrl)
// Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied'))
$e('c:shared-base:copy-url')
}
const clickInviteMore = () => {
$e('c:user:invite-more')
usersData.invitationToken = undefined
usersData.role = Role.OrgLevelViewer
usersData.emails = ''
}
</script>
<template>
<a-modal
:footer="null"
centered
:visible="show"
:closable="false"
width="max(50vw, 44rem)"
wrap-class-name="nc-modal-invite-user-and-share-base"
@cancel="emit('closed')"
>
<div class="flex flex-col">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
<a-typography-title class="select-none" :level="4"> {{ $t('activity.inviteUser') }}
</a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
</div>
<div class="px-2 mt-1.5">
<template v-if="usersData.invitationToken">
<div class="flex flex-col mt-1 border-b-1 pb-5">
<div class="flex flex-row items-center pl-1.5 pb-1 h-[1.1rem]">
<MdiAccountOutline />
<div class="text-xs ml-0.5 mt-0.5">Copy Invite Token</div>
</div>
<a-alert class="mt-1" type="success" show-icon>
<template #message>
<div class="flex flex-row justify-between items-center py-1">
<div class="flex pl-2 text-green-700 text-xs">
{{ inviteUrl }}
</div>
<a-button type="text" class="!rounded-md -mt-0.5" @click="copyUrl">
<template #icon>
<MdiContentCopy class="flex mx-auto text-green-700 h-[1rem]" />
</template>
</a-button>
</div>
</template>
</a-alert>
<div class="flex text-xs text-gray-500 mt-2 justify-start ml-2">
{{ $t('msg.info.userInviteNoSMTP') }}
{{ usersData.invitationToken && usersData.emails }}
</div>
<div class="flex flex-row justify-start mt-4 ml-2">
<a-button size="small" outlined @click="clickInviteMore">
<div class="flex flex-row justify-center items-center space-x-0.5">
<MaterialSymbolsSendOutline class="flex mx-auto text-gray-600 h-[0.8rem]" />
<div class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div>
</div>
</a-button>
</div>
</div>
</template>
<div v-else class="flex flex-col pb-4">
<div class="flex flex-row items-center pl-2 pb-1 h-[1rem]">
<MdiAccountOutline />
<div class="text-xs ml-0.5 mt-0.5">{{$t('activity.inviteUser') }}
</div>
</div>
<div class="border-1 py-3 px-4 rounded-md mt-1">
<a-form
ref="formRef"
:validate-on-rule-change="false"
:model="usersData"
validate-trigger="onBlur"
@finish="saveUser"
>
<div class="flex flex-row space-x-4">
<div class="flex flex-col w-3/4">
<a-form-item
v-bind="validateInfos.emails"
validate-trigger="onBlur"
name="emails"
:rules="[{ required: true, message: 'Please input email' }]"
>
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('datatype.Email') }}:</div>
<a-input
v-model:value="usersData.emails"
validate-trigger="onBlur"
:placeholder="$t('labels.email')"
/>
</a-form-item>
</div>
<div class="flex flex-col w-1/4">
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role" class="nc-user-roles"
dropdown-class-name="nc-dropdown-user-role">
<a-select-option v-for="(role, index) in [Role.OrgLevelViewer, Role.OrgLevelCreator]" :key="index" :value="role"
class="nc-role-option">
<div class="flex flex-row h-full justify-start items-center">
<div
class="px-2 py-1 flex rounded-full text-xs"
:style="{ backgroundColor: projectRoleTagColors[role] }"
>
{{ role }}
</div>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
</div>
<div class="flex flex-row justify-center">
<a-button type="primary" html-type="submit">
<div class="flex flex-row justify-center items-center space-x-1.5">
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
<div>{{ $t('activity.invite') }}</div>
</div>
</a-button>
</div>
</a-form>
</div>
</div>
</div>
</div>
</a-modal>
</template>

108
packages/nc-gui/components/org-user/index.vue

@ -1,20 +1,19 @@
<script lang="ts" setup>
import { message, Modal } from 'ant-design-vue'
import type { RequestParams } from 'nocodb-sdk'
import type { User } from '#imports'
import { Role, extractSdkResponseErrorMsg, useNuxtApp } from '#imports'
import { Modal, message } from 'ant-design-vue'
import type { RequestParams, UserType } from 'nocodb-sdk'
import { Role, extractSdkResponseErrorMsg } from '#imports'
import { useApi } from '~/composables/useApi'
const { api, isLoading } = useApi()
let users = $ref<null | User[]>(null)
let totalRows = $ref(0)
let users = $ref<UserType[]>([])
const currentPage = $ref(1)
const currentLimit = $ref(10)
const showUserModal = ref(false)
const searchText = ref<string>('')
const pagination = reactive({
@ -35,20 +34,19 @@ const loadUsers = async (page = currentPage, limit = currentLimit) => {
pagination.total = response.pageInfo.totalRows ?? 0
pagination.pageSize = 10
users = response.list as User[]
users = response.list as UserType[]
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
loadUsers()
const updateRole = async (userId: string, roles: Role) => {
try {
await api.orgUsers.update(userId, {
roles,
} as unknown as User)
} as unknown as UserType)
message.success('Role updated successfully')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -71,14 +69,23 @@ const deleteUser = async (userId: string) => {
</script>
<template>
<div class=" h-full overflow-y-scroll scrollbar-thin-dull">
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<div class="max-w-[700px] mx-auto p-4">
<div class="py-2 flex"><a-input-search size="small" class=" max-w-[300px]" placeholder="Filter by email" v-model:value="searchText"
@blur="loadUsers" @keydown.enter="loadUsers"></a-input-search>
<div class="py-2 flex">
<a-input-search
v-model:value="searchText"
size="small"
class="max-w-[300px]"
placeholder="Filter by email"
@blur="loadUsers"
@keydown.enter="loadUsers"
>
</a-input-search>
<div class="flex-grow"></div>
<a-button size="small">
<a-button size="small" @click="showUserModal = true">
<div class="flex items-center gap-1">
<MdiAdd/> Add new user
<MdiAdd />
Invite new user
</div>
</a-button>
</div>
@ -106,13 +113,15 @@ const deleteUser = async (userId: string) => {
<a-table-column key="roles" :title="$t('objects.role')" data-index="roles">
<template #default="{ record }">
<div>
<div v-if="record.roles.includes('super')" class="font-weight-bold">Super Admin</div>
<a-select
v-else
v-model:value="record.roles"
class="min-w-[220px]"
:options="[
{ value: Role.OrgLevelCreator, label: $t(`objects.roleType.orgLevelCreator`) },
{ value: Role.OrgLevelViewer, label: $t(`objects.roleType.orgLevelViewer`) },
]"
{ value: Role.OrgLevelCreator, label: $t(`objects.roleType.orgLevelCreator`) },
{ value: Role.OrgLevelViewer, label: $t(`objects.roleType.orgLevelViewer`) },
]"
@change="updateRole(record.id, record.roles)"
>
</a-select>
@ -140,68 +149,7 @@ const deleteUser = async (userId: string) => {
</a-table-column>
</a-table>
<!-- <div class="py-4"> -->
<!-- <a-input-search class="mx-w-[300px]" v-model="searchText" @search="loadUsers" /> -->
<!-- </div> -->
<!-- <div class="px-5">
<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">
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('labels.email') }}</div>
</div>
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('object.projects') }}</div>
</div>
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1">
<MdiDramaMasks class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs">{{ $t('objects.role') }}</div>
</div>
<div class="flex flex-row w-1/6 justify-end items-center pl-1">
<div class="text-gray-600 text-xs">{{ $t('labels.actions') }}</div>
</div>
</div>
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2 nc-user-row">
<div class="flex w-4/6 flex-wrap nc-user-email">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
{{ user.projectsCount }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
&lt;!&ndash; <div v-if="user.roles" class="rounded-full px-2 py-1 nc-user-role">
{{ $t(`objects.roleType.${user.roles.split(',')[0].replace(/-(\w)/g, (_, m1) => m1.toUpperCase())}`) }}
</div>&ndash;&gt;
<a-select
class="min-w-[220px]"
:options="[
{ value: Role.OrgLevelCreator, label: $t(`objects.roleType.orgLevelCreator`) },
{ value: Role.OrgLevelViewer, label: $t(`objects.roleType.orgLevelViewer`) },
]"
>
</a-select>
</div>
<div class="flex w-1/6 flex-wrap justify-end">
<MdiDeleteOutline />
</div>
</div>
<a-pagination
v-model:current="currentPage"
hide-on-single-page
class="mt-4"
:page-size="currentLimit"
:total="totalRows"
show-less-items
@change="loadUsers"
/>
</div> -->
<LazyOrgUserUsersModal :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div>
</div>
</template>

1
packages/nc-gui/lang/en.json

@ -343,6 +343,7 @@
"invite": "Invite",
"inviteMore": "Invite more",
"inviteTeam": "Invite Team",
"inviteUser": "Invite User",
"inviteToken": "Invite Token",
"newUser": "New User",
"editUser": "Edit user",

Loading…
Cancel
Save