Browse Source

fix/follow up on user management

pull/3067/head
Muhammed Mustafa 2 years ago
parent
commit
eef3a7a69e
  1. 22
      packages/nc-gui-v2/components/tabs/auth/UserManagement.vue
  2. 20
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  3. 34
      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. 6
      packages/nc-gui-v2/lib/enums.ts
  7. 2
      packages/nc-gui-v2/utils/urlUtils.ts
  8. 6
      packages/nc-gui-v2/utils/userUtils.ts

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

@ -22,6 +22,7 @@ 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()
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 +81,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))
} }
@ -158,8 +160,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 +181,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 +194,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 +211,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[role] }">
{{ 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 +267,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>

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

@ -26,7 +26,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 {
@ -145,14 +145,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>
@ -162,7 +162,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>
@ -172,7 +172,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>
@ -182,7 +182,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>
@ -197,8 +197,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>
@ -206,8 +206,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>

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

@ -32,7 +32,7 @@ const { project } = useProject()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { copy } = useClipboard() const { copy } = useClipboard()
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 +100,7 @@ const saveUser = async () => {
} }
const inviteUrl = $computed(() => const inviteUrl = $computed(() =>
usersData.invitationToken usersData.invitationToken ? `${location.origin}/user/authentication/signup/${usersData.invitationToken}` : null,
? `${location.origin}${location.pathname}#/user/authentication/signup/${usersData.invitationToken}`
: null,
) )
const copyUrl = async () => { const copyUrl = async () => {
@ -117,7 +115,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 +124,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 +142,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 +154,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 +194,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 Object.keys(projectRoleTagColors)" :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 +219,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",

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

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

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

@ -18,7 +18,7 @@ export const replaceUrlsWithLink = (text: string): boolean | string => {
} }
export const dashboardUrl = () => { export const dashboardUrl = () => {
return `${location.origin}${location.pathname || ''}` return `${location.origin}`
} }
// ref : https://stackoverflow.com/a/5717133 // ref : https://stackoverflow.com/a/5717133

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

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

Loading…
Cancel
Save