Browse Source

Merge pull request #3067 from nocodb/fix/follow-up-user-management

vue3: Follow up on User management
pull/3093/head
Raju Udava 2 years ago committed by GitHub
parent
commit
af81b1b169
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      packages/nc-gui-v2/components/tabs/auth/UserManagement.vue
  2. 23
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  3. 39
      packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue
  4. 1
      packages/nc-gui-v2/composables/useUIPermission/rolePermissions.ts
  5. 1
      packages/nc-gui-v2/lang/en.json
  6. 5
      packages/nc-gui-v2/lib/enums.ts
  7. 4
      packages/nc-gui-v2/utils/urlUtils.ts
  8. 7
      packages/nc-gui-v2/utils/userUtils.ts

25
packages/nc-gui-v2/components/tabs/auth/UserManagement.vue

@ -22,6 +22,8 @@ const toast = useToast()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { project } = useProject() const { project } = useProject()
const { copy } = useClipboard() const { copy } = useClipboard()
const { isUIAllowed } = useUIPermission()
const { dashboardUrl } = $(useDashboard())
let users = $ref<null | User[]>(null) let users = $ref<null | User[]>(null)
let selectedUser = $ref<null | User>(null) let selectedUser = $ref<null | User>(null)
@ -80,6 +82,7 @@ const deleteUser = async () => {
await loadUsers() await loadUsers()
showUserDeleteModal = false showUserDeleteModal = false
} catch (e: any) { } catch (e: any) {
showUserDeleteModal = false
console.error(e) console.error(e)
toast.error(await extractSdkResponseErrorMsg(e)) toast.error(await extractSdkResponseErrorMsg(e))
} }
@ -120,7 +123,7 @@ const resendInvite = async (user: User) => {
const copyInviteUrl = (user: User) => { const copyInviteUrl = (user: User) => {
if (!user.invite_token) return if (!user.invite_token) return
const getInviteUrl = (token: string) => `${location.origin}${location.pathname}#/user/authentication/signup/${token}` const getInviteUrl = (token: string) => `${dashboardUrl}/user/authentication/signup/${token}`
copy(getInviteUrl(user.invite_token)) copy(getInviteUrl(user.invite_token))
toast.success('Invite url copied to clipboard') toast.success('Invite url copied to clipboard')
@ -158,8 +161,8 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
This action will remove this user from this project This action will remove this user from this project
</div> </div>
<div class="flex mt-6 justify-center space-x-2"> <div class="flex mt-6 justify-center space-x-2">
<a-button @click="showUserDeleteModal = false"> Cancel </a-button> <a-button @click="showUserDeleteModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="deleteUser"> Confirm </a-button> <a-button type="primary" danger @click="deleteUser"> {{ $t('general.confirm') }} </a-button>
</div> </div>
</div> </div>
</a-modal> </a-modal>
@ -179,10 +182,10 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="text-gray-500">Reload</div> <div class="text-gray-500">Reload</div>
</div> </div>
</a-button> </a-button>
<a-button size="middle" @click="onInvite"> <a-button v-if="isUIAllowed('newUser')" size="middle" @click="onInvite">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1"> <div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MidAccountIcon /> <MidAccountIcon />
<div>Invite Team</div> <div>{{ $t('activity.inviteTeam') }}</div>
</div> </div>
</a-button> </a-button>
</div> </div>
@ -192,15 +195,15 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1"> <div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EmailIcon class="flex text-gray-500 -mt-0.5" /> <EmailIcon class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">E-mail</div> <div class="text-gray-600 text-xs space-x-1">{{ $t('labels.email') }}</div>
</div> </div>
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1"> <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" /> <RolesIcon class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs">Role</div> <div class="text-gray-600 text-xs">{{ $t('objects.role') }}</div>
</div> </div>
<div class="flex flex-row w-1/6 justify-end items-center pl-1"> <div class="flex flex-row w-1/6 justify-end items-center pl-1">
<div class="text-gray-600 text-xs">Actions</div> <div class="text-gray-600 text-xs">{{ $t('labels.actions') }}</div>
</div> </div>
</div> </div>
@ -209,14 +212,14 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
{{ user.email }} {{ user.email }}
</div> </div>
<div class="flex w-1/6 justify-center flex-wrap ml-4"> <div class="flex w-1/6 justify-center flex-wrap ml-4">
<div :class="`rounded-full px-2 py-1 bg-[${projectRoleTagColors[user.roles]}]`"> <div class="rounded-full px-2 py-1" :style="{ backgroundColor: projectRoleTagColors[user.roles as String] }">
{{ user.roles }} {{ user.roles }}
</div> </div>
</div> </div>
<div class="flex w-1/6 flex-wrap justify-end"> <div class="flex w-1/6 flex-wrap justify-end">
<a-tooltip v-if="user.project_id" placement="bottom"> <a-tooltip v-if="user.project_id" placement="bottom">
<template #title> <template #title>
<span>Edit user</span> <span>{{ $t('activity.editUser') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md" @click="onEdit(user)"> <a-button type="text" class="!rounded-md" @click="onEdit(user)">
<template #icon> <template #icon>
@ -265,7 +268,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<a-menu-item> <a-menu-item>
<div class="flex flex-row items-center py-1" @click="copyInviteUrl(user)"> <div class="flex flex-row items-center py-1" @click="copyInviteUrl(user)">
<MdiContentCopyIcon class="flex h-[1rem]" /> <MdiContentCopyIcon class="flex h-[1rem]" />
<div class="text-xs pl-2">Copy invite URL</div> <div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div> </div>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>

23
packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue

@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { onMounted, useClipboard, useNuxtApp, useProject } from '#imports' import { onMounted, useClipboard, useNuxtApp, useProject } from '#imports'
import { dashboardUrl, extractSdkResponseErrorMsg } from '~/utils' import { extractSdkResponseErrorMsg } from '~/utils'
const toast = useToast() const toast = useToast()
const { dashboardUrl } = $(useDashboard())
interface ShareBase { interface ShareBase {
uuid?: string uuid?: string
@ -26,7 +27,7 @@ const { project } = useProject()
const { copy } = useClipboard() const { copy } = useClipboard()
const url = $computed(() => (base && base.uuid ? `${dashboardUrl()}#/nc/base/${base.uuid}` : null)) const url = $computed(() => (base && base.uuid ? `${dashboardUrl}/nc/base/${base.uuid}` : null))
const loadBase = async () => { const loadBase = async () => {
try { try {
@ -143,14 +144,14 @@ onMounted(() => {
<div class="flex flex-row items-center space-x-0.5 pl-2 h-[0.8rem]"> <div class="flex flex-row items-center space-x-0.5 pl-2 h-[0.8rem]">
<MdiOpenInNew /> <MdiOpenInNew />
<div class="text-xs">Shared Base Link</div> <div class="text-xs">{{ $t('activity.shareBase.link') }}</div>
</div> </div>
<div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between"> <div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between">
<span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2">{{ url }}</span> <span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2">{{ url }}</span>
<div class="flex border-l-1 pt-1 pl-1"> <div class="flex border-l-1 pt-1 pl-1">
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>Reload</span> <span>{{ $t('general.reload') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="recreate"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="recreate">
<template #icon> <template #icon>
@ -160,7 +161,7 @@ onMounted(() => {
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>Copy URL</span> <span>{{ $t('activity.copyUrl') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="copyUrl"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="copyUrl">
<template #icon> <template #icon>
@ -170,7 +171,7 @@ onMounted(() => {
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>Open new tab</span> <span>{{ $t('activity.openTab') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="navigateToSharedBase"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="navigateToSharedBase">
<template #icon> <template #icon>
@ -180,7 +181,7 @@ onMounted(() => {
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>Copy embeddable HTML code</span> <span>{{ $t('activity.iFrame') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="generateEmbeddableIframe"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="generateEmbeddableIframe">
<template #icon> <template #icon>
@ -195,8 +196,8 @@ onMounted(() => {
<a-dropdown v-model="showEditBaseDropdown" class="flex"> <a-dropdown v-model="showEditBaseDropdown" class="flex">
<a-button> <a-button>
<div class="flex flex-row items-center space-x-2"> <div class="flex flex-row items-center space-x-2">
<div v-if="base?.uuid">Anyone with the link</div> <div v-if="base?.uuid">{{ $t('activity.shareBase.enable') }}</div>
<div v-else>Disable shared base</div> <div v-else>{{ $t('activity.shareBase.disable') }}</div>
<IcRoundKeyboardArrowDown class="h-[1rem]" /> <IcRoundKeyboardArrowDown class="h-[1rem]" />
</div> </div>
</a-button> </a-button>
@ -204,8 +205,8 @@ onMounted(() => {
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item> <a-menu-item>
<div v-if="base?.uuid" @click="disableSharedBase">Disable shared base</div> <div v-if="base?.uuid" @click="disableSharedBase">{{ $t('activity.shareBase.disable') }}</div>
<div v-else @click="createShareBase(ShareBaseRole.Viewer)">Anyone with the link</div> <div v-else @click="createShareBase(ShareBaseRole.Viewer)">{{ $t('activity.shareBase.enable') }}</div>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</template> </template>

39
packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue

@ -9,7 +9,7 @@ import MidAccountIcon from '~icons/mdi/account-outline'
import ContentCopyIcon from '~icons/mdi/content-copy' import ContentCopyIcon from '~icons/mdi/content-copy'
import type { User } from '~/lib/types' import type { User } from '~/lib/types'
import { ProjectRole } from '~/lib/enums' import { ProjectRole } from '~/lib/enums'
import { projectRoleTagColors } from '~/utils/userUtils' import { projectRoleTagColors, projectRoles } from '~/utils/userUtils'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { isEmail } from '~/utils/validation' import { isEmail } from '~/utils/validation'
@ -31,8 +31,9 @@ const toast = useToast()
const { project } = useProject() const { project } = useProject()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { copy } = useClipboard() const { copy } = useClipboard()
const { dashboardUrl } = $(useDashboard())
const usersData = $ref<Users>({ emails: undefined, role: ProjectRole.Guest, invitationToken: undefined }) const usersData = $ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invitationToken: undefined })
const formRef = ref() const formRef = ref()
const useForm = Form.useForm const useForm = Form.useForm
@ -100,9 +101,7 @@ const saveUser = async () => {
} }
const inviteUrl = $computed(() => const inviteUrl = $computed(() =>
usersData.invitationToken usersData.invitationToken ? `${dashboardUrl}/user/authentication/signup/${usersData.invitationToken}` : null,
? `${location.origin}${location.pathname}#/user/authentication/signup/${usersData.invitationToken}`
: null,
) )
const copyUrl = async () => { const copyUrl = async () => {
@ -117,7 +116,7 @@ const copyUrl = async () => {
const clickInviteMore = () => { const clickInviteMore = () => {
$e('c:user:invite-more') $e('c:user:invite-more')
usersData.invitationToken = undefined usersData.invitationToken = undefined
usersData.role = ProjectRole.Guest usersData.role = ProjectRole.Viewer
usersData.emails = undefined usersData.emails = undefined
} }
</script> </script>
@ -126,7 +125,7 @@ const clickInviteMore = () => {
<a-modal :footer="null" centered :visible="show" :closable="false" width="max(50vw, 44rem)" @cancel="emit('closed')"> <a-modal :footer="null" centered :visible="show" :closable="false" width="max(50vw, 44rem)" @cancel="emit('closed')">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full"> <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"> Share: {{ project.title }} </a-typography-title> <a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon> <template #icon>
<CloseIcon class="flex mx-auto" /> <CloseIcon class="flex mx-auto" />
@ -144,11 +143,11 @@ const clickInviteMore = () => {
<a-alert class="mt-1" type="success" show-icon> <a-alert class="mt-1" type="success" show-icon>
<template #message> <template #message>
<div class="flex flex-row w-full justify-between items-center"> <div class="flex flex-row justify-between items-center py-1">
<div class="flex pl-2 text-green-700"> <div class="flex pl-2 text-green-700 text-xs">
{{ inviteUrl }} {{ inviteUrl }}
</div> </div>
<a-button type="text" class="!rounded-md mr-1" @click="copyUrl"> <a-button type="text" class="!rounded-md -mt-0.5" @click="copyUrl">
<template #icon> <template #icon>
<ContentCopyIcon class="flex mx-auto text-green-700 h-[1rem]" /> <ContentCopyIcon class="flex mx-auto text-green-700 h-[1rem]" />
</template> </template>
@ -156,15 +155,16 @@ const clickInviteMore = () => {
</div> </div>
</template> </template>
</a-alert> </a-alert>
<div class="flex text-xs text-gray-500 mt-2 justify-start ml-2"> <div class="flex text-xs text-gray-500 mt-2 justify-start ml-2">
Looks like you have not configured mailer yet! Please copy above invite link and send it to {{ $t('msg.info.userInviteNoSMTP') }}
{{ usersData.invitationToken && usersData.emails }} {{ usersData.invitationToken && usersData.emails }}
</div> </div>
<div class="flex flex-row justify-start mt-4 ml-2"> <div class="flex flex-row justify-start mt-4 ml-2">
<a-button size="small" outlined @click="clickInviteMore"> <a-button size="small" outlined @click="clickInviteMore">
<div class="flex flex-row justify-center items-center space-x-0.5"> <div class="flex flex-row justify-center items-center space-x-0.5">
<SendIcon class="flex mx-auto text-gray-600 h-[0.8rem]" /> <SendIcon class="flex mx-auto text-gray-600 h-[0.8rem]" />
<div class="text-xs text-gray-600">Invite more</div> <div class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div>
</div> </div>
</a-button> </a-button>
</div> </div>
@ -195,18 +195,21 @@ const clickInviteMore = () => {
<a-input <a-input
v-model:value="usersData.emails" v-model:value="usersData.emails"
validate-trigger="onBlur" validate-trigger="onBlur"
placeholder="Email" :placeholder="$t('labels.email')"
:disabled="!!selectedUser" :disabled="!!selectedUser"
/> />
</a-form-item> </a-form-item>
</div> </div>
<div class="flex flex-col w-1/4"> <div class="flex flex-col w-1/4">
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]"> <a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
<div class="ml-1 mb-1 text-xs text-gray-500">Select User Role:</div> <div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role"> <a-select v-model:value="usersData.role">
<a-select-option v-for="(role, index) in Object.keys(projectRoleTagColors)" :key="index" :value="role"> <a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role">
<div class="flex flex-row h-full justify-start items-center"> <div class="flex flex-row h-full justify-start items-center">
<div :class="`px-2 py-1 flex rounded-full text-xs bg-[${projectRoleTagColors[role]}]`"> <div
class="px-2 py-1 flex rounded-full text-xs"
:style="{ backgroundColor: projectRoleTagColors[role] }"
>
{{ role }} {{ role }}
</div> </div>
</div> </div>
@ -217,10 +220,10 @@ const clickInviteMore = () => {
</div> </div>
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
<a-button type="primary" html-type="submit"> <a-button type="primary" html-type="submit">
<div v-if="selectedUser">Save</div> <div v-if="selectedUser">{{ $t('general.save') }}</div>
<div v-else class="flex flex-row justify-center items-center space-x-1.5"> <div v-else class="flex flex-row justify-center items-center space-x-1.5">
<SendIcon class="flex h-[0.8rem]" /> <SendIcon class="flex h-[0.8rem]" />
<div>Invite</div> <div>{{ $t('activity.invite') }}</div>
</div> </div>
</a-button> </a-button>
</div> </div>

1
packages/nc-gui-v2/composables/useUIPermission/rolePermissions.ts

@ -17,6 +17,7 @@ const rolePermissions = {
csvImport: true, csvImport: true,
apiDocs: true, apiDocs: true,
projectSettings: true, projectSettings: true,
newUser: false,
}, },
commenter: { commenter: {
smartSheet: true, smartSheet: true,

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

@ -13,6 +13,7 @@
"edit": "Edit", "edit": "Edit",
"remove": "Remove", "remove": "Remove",
"save": "Save", "save": "Save",
"confirm": "Confirm",
"cancel": "Cancel", "cancel": "Cancel",
"submit": "Submit", "submit": "Submit",
"create": "Create", "create": "Create",

5
packages/nc-gui-v2/lib/enums.ts

@ -6,9 +6,10 @@ export enum Role {
export enum ProjectRole { export enum ProjectRole {
Owner = 'owner', Owner = 'owner',
Creator = 'creator',
Editor = 'editor', Editor = 'editor',
User = 'user', Commenter = 'commenter',
Guest = 'guest', Viewer = 'viewer',
} }
export enum ClientType { export enum ClientType {

4
packages/nc-gui-v2/utils/urlUtils.ts

@ -17,10 +17,6 @@ export const replaceUrlsWithLink = (text: string): boolean | string => {
return found && out return found && out
} }
export const dashboardUrl = () => {
return `${location.origin}${location.pathname || ''}`
}
// ref : https://stackoverflow.com/a/5717133 // ref : https://stackoverflow.com/a/5717133
export const isValidURL = (str: string) => { export const isValidURL = (str: string) => {
const pattern = const pattern =

7
packages/nc-gui-v2/utils/userUtils.ts

@ -2,7 +2,10 @@ import { ProjectRole } from '~/lib/enums'
export const projectRoleTagColors = { export const projectRoleTagColors = {
[ProjectRole.Owner]: '#cfdffe', [ProjectRole.Owner]: '#cfdffe',
[ProjectRole.Creator]: '#d0f1fd',
[ProjectRole.Editor]: '#c2f5e8', [ProjectRole.Editor]: '#c2f5e8',
[ProjectRole.User]: '#4caf50', [ProjectRole.Commenter]: '#ffdaf6',
[ProjectRole.Guest]: '#9e9e9e', [ProjectRole.Viewer]: '#ffdce5',
} }
export const projectRoles = [ProjectRole.Creator, ProjectRole.Editor, ProjectRole.Commenter, ProjectRole.Viewer]

Loading…
Cancel
Save