Browse Source

Merge pull request #6378 from nocodb/nc-fix/inherited-role

feat: new role badges and selector
pull/6475/head
mertmit 1 year ago committed by GitHub
parent
commit
c0e687e17b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  2. 14
      packages/nc-gui/components/nc/Badge.vue
  3. 125
      packages/nc-gui/components/project/AccessSettings.vue
  4. 57
      packages/nc-gui/components/project/InviteProjectCollabSection.vue
  5. 74
      packages/nc-gui/components/roles/Badge.vue
  6. 57
      packages/nc-gui/components/roles/Selector.vue
  7. 71
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  8. 38
      packages/nc-gui/components/workspace/InviteSection.vue
  9. 11
      packages/nc-gui/components/workspace/View.vue
  10. 8
      packages/nc-gui/composables/useRoles/index.ts
  11. 6
      packages/nc-gui/middleware/auth.global.ts
  12. 13
      packages/nc-gui/store/users.ts
  13. 19
      packages/nc-gui/store/workspace.ts
  14. 13
      packages/nc-gui/utils/iconUtils.ts
  15. 42
      packages/nocodb-sdk/src/lib/enums.ts
  16. 2
      packages/nocodb/src/guards/global/global.guard.ts
  17. 3
      packages/nocodb/src/models/User.ts
  18. 4
      packages/nocodb/src/services/project-users/project-users.service.ts
  19. 2
      packages/nocodb/src/strategies/google.strategy/google.strategy.ts
  20. 2
      packages/nocodb/src/utils/roleHelper.ts
  21. 6
      tests/playwright/pages/Dashboard/ProjectView/AccessSettingsPage.ts
  22. 4
      tests/playwright/pages/WorkspacePage/CollaborationPage.ts
  23. 2
      tests/playwright/setup/index.ts

6
packages/nc-gui/components/dashboard/Sidebar/TopSection.vue

@ -2,9 +2,11 @@
const workspaceStore = useWorkspace()
const projectStore = useProject()
const { isUIAllowed } = useRoles()
const { appInfo } = useGlobal()
const { isWorkspaceLoading, isWorkspaceOwnerOrCreator, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore)
const { isWorkspaceLoading, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore)
const { navigateToWorkspaceSettings } = workspaceStore
@ -45,7 +47,7 @@ const navigateToSettings = () => {
<DashboardSidebarTopSectionHeader />
<NcButton
v-if="isWorkspaceOwnerOrCreator"
v-if="isUIAllowed('workspaceSettings')"
type="text"
size="small"
class="nc-sidebar-top-button"

14
packages/nc-gui/components/nc/Badge.vue

@ -1,19 +1,27 @@
<script lang="ts" setup>
const props = defineProps<{
const props = withDefaults(
defineProps<{
color: string
}>()
border?: boolean
}>(),
{
border: true,
},
)
</script>
<template>
<div
class="border-1 h-6 rounded-md px-1"
class="h-6 rounded-md px-1"
:class="{
'border-purple-500 bg-purple-100': props.color === 'purple',
'border-blue-500 bg-blue-100': props.color === 'blue',
'border-green-500 bg-green-100': props.color === 'green',
'border-orange-500 bg-orange-100': props.color === 'orange',
'border-yellow-500 bg-yellow-100': props.color === 'yellow',
'border-red-500 bg-red-100': props.color === 'red',
'border-gray-300': !props.color,
'border-1': props.border,
}"
>
<slot />

125
packages/nc-gui/components/project/AccessSettings.vue

@ -1,15 +1,25 @@
<script lang="ts" setup>
import type { WorkspaceUserType } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles, RoleColors, RoleLabels } from 'nocodb-sdk'
import type { OrgUserRoles, WorkspaceUserRoles } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles, WorkspaceRolesToProjectRoles } from 'nocodb-sdk'
import InfiniteLoading from 'v3-infinite-loading'
import { isEeUI, storeToRefs, stringToColour, timeAgo, useGlobal } from '#imports'
import { isEeUI, storeToRefs, stringToColour, timeAgo } from '#imports'
const { user } = useGlobal()
const projectsStore = useProjects()
const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = projectsStore
const { activeProjectId } = storeToRefs(projectsStore)
const collaborators = ref<WorkspaceUserType[]>([])
const { projectRoles } = useRoles()
const collaborators = ref<
{
id: string
email: string
main_roles: OrgUserRoles
roles: ProjectRoles
workspace_roles: WorkspaceUserRoles
created_at: string
}[]
>([])
const totalCollaborators = ref(0)
const userSearchText = ref('')
const currentPage = ref(0)
@ -34,9 +44,12 @@ const loadCollaborators = async () => {
...collaborators.value,
...users.map((user: any) => ({
...user,
projectRoles: user.roles,
// TODO: Remove this hack and make the values consistent with the backend
roles: user.roles ?? (RoleLabels[user.workspace_roles as string] as string)?.toLowerCase() ?? ProjectRoles.NO_ACCESS,
project_roles: user.roles,
roles:
user.roles ??
(user.workspace_roles
? WorkspaceRolesToProjectRoles[user.workspace_roles as WorkspaceUserRoles] ?? ProjectRoles.NO_ACCESS
: ProjectRoles.NO_ACCESS),
})),
]
} catch (e: any) {
@ -62,19 +75,40 @@ const loadListData = async ($state: any) => {
$state.loaded()
}
const updateCollaborator = async (collab, roles) => {
const reloadCollabs = async () => {
currentPage.value = 0
collaborators.value = []
await loadCollaborators()
}
const updateCollaborator = async (collab: any, roles: ProjectRoles) => {
try {
if (!roles || roles === 'inherit' || (roles === ProjectRoles.NO_ACCESS && !isEeUI)) {
if (
!roles ||
(roles === ProjectRoles.NO_ACCESS && !isEeUI) ||
(collab.workspace_roles && WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles] === roles && isEeUI)
) {
await removeProjectUser(activeProjectId.value!, collab)
collab.projectRoles = null
} else if (collab.projectRoles) {
if (
collab.workspace_roles &&
WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles] === roles &&
isEeUI
) {
collab.roles = WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles]
} else {
collab.roles = ProjectRoles.NO_ACCESS
}
} else if (collab.project_roles) {
collab.roles = roles
await updateProjectUser(activeProjectId.value!, collab)
} else {
collab.roles = roles
await createProjectUser(activeProjectId.value!, collab)
collab.projectRoles = roles
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
reloadCollabs()
}
}
@ -101,22 +135,13 @@ watchDebounced(
},
)
const reloadCollabs = async () => {
currentPage.value = 0
collaborators.value = []
await loadCollaborators()
}
const userProjectRole = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles]>(() => {
const projectUser = collaborators.value?.find((collab) => collab.id === user.value?.id)
return projectUser?.projectRoles
})
onMounted(async () => {
isLoading.value = true
try {
await loadCollaborators()
const currentRoleIndex = OrderedProjectRoles.findIndex((role) => role === userProjectRole.value)
const currentRoleIndex = OrderedProjectRoles.findIndex(
(role) => projectRoles.value && Object.keys(projectRoles.value).includes(role),
)
if (currentRoleIndex !== -1) {
accessibleRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1)
}
@ -180,41 +205,23 @@ onMounted(async () => {
{{ timeAgo(collab.created_at) }}
</div>
<div class="w-1/5 roles">
<div class="nc-collaborator-role-select">
<NcSelect
v-model:value="collab.roles"
class="w-35 !rounded px-1"
:virtual="true"
:placeholder="$t('labels.noAccess')"
:disabled="collab.id === user?.id || (collab.roles && !accessibleRoles.includes(collab.roles))"
@change="(value) => updateCollaborator(collab, value)"
>
<template #suffixIcon>
<MdiChevronDown />
<div class="nc-collaborator-role-select p-2">
<template v-if="accessibleRoles.includes(collab.roles)">
<RolesSelector
:role="collab.roles"
:roles="accessibleRoles"
:inherit="
isEeUI && collab.workspace_roles && WorkspaceRolesToProjectRoles[collab.workspace_roles]
? WorkspaceRolesToProjectRoles[collab.workspace_roles]
: null
"
:description="false"
:on-role-change="(role: ProjectRoles) => updateCollaborator(collab, role)"
/>
</template>
<a-select-option v-if="collab.id === user?.id" :value="userProjectRole">
<NcBadge :color="RoleColors[userProjectRole]">
<p class="badge-text">{{ RoleLabels[userProjectRole] }}</p>
</NcBadge>
</a-select-option>
<a-select-option v-if="collab.roles && !accessibleRoles.includes(collab.roles)" :value="collab.roles">
<NcBadge :color="RoleColors[collab.roles]">
<p class="badge-text">{{ RoleLabels[collab.roles] }}</p>
</NcBadge>
</a-select-option>
<template v-for="role of accessibleRoles" :key="`role-option-${role}`">
<a-select-option :value="role">
<NcBadge :color="RoleColors[role]">
<p class="badge-text">{{ RoleLabels[role] }}</p>
</NcBadge>
</a-select-option>
<template v-else>
<RolesBadge class="!bg-white" :role="collab.roles" />
</template>
<a-select-option v-if="isEeUI" value="inherit">
<NcBadge color="white">
<p class="badge-text">Inherit</p>
</NcBadge>
</a-select-option>
</NcSelect>
</div>
</div>
<div class="w-1/5"></div>
@ -238,7 +245,7 @@ onMounted(async () => {
<style scoped lang="scss">
.badge-text {
@apply text-[14px] pt-1 text-center;
@apply text-[14px] flex items-center justify-center gap-1 pt-0.5;
}
.nc-collaborators-list {

57
packages/nc-gui/components/project/InviteProjectCollabSection.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ProjectRoles } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, useDashboard, useManageUsers } from '#imports'
const emit = defineEmits(['invited'])
@ -15,6 +15,8 @@ const { inviteUser } = useManageUsers()
const { $e } = useNuxtApp()
const { projectRoles } = useRoles()
const usersData = ref<{
invite_token?: string
email?: string
@ -47,6 +49,22 @@ const inviteUrl = computed(() =>
usersData.value?.invite_token ? `${dashboardUrl.value}#/signup/${usersData.value.invite_token}` : null,
)
// allow only lower roles to be assigned
const allowedRoles = ref<ProjectRoles[]>([])
onMounted(async () => {
try {
const currentRoleIndex = OrderedProjectRoles.findIndex(
(role) => projectRoles.value && Object.keys(projectRoles.value).includes(role),
)
if (currentRoleIndex !== -1) {
allowedRoles.value = OrderedProjectRoles.slice(currentRoleIndex).filter((r) => r && r !== ProjectRoles.OWNER)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
})
const { copy } = useCopy(true)
const { t } = useI18n()
@ -117,36 +135,13 @@ const copyUrl = async () => {
class="!max-w-130 !rounded"
/>
<NcSelect v-model:value="inviteData.roles" class="min-w-30 !rounded px-1" data-testid="roles">
<template #suffixIcon>
<MdiChevronDown />
</template>
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option">
<!-- <div
class="flex flex-row h-full justify-start items-center"
:data-testid="`nc-share-invite-user-role-option-${role}`"
>
<div class="px-2 py-1 flex rounded-full text-xs capitalize">
{{ role }}
</div>
</div> -->
<NcBadge v-if="role === ProjectRoles.OWNER" color="purple">
<p class="badge-text">{{ role }}</p>
</NcBadge>
<NcBadge v-if="role === ProjectRoles.CREATOR" color="blue">
<p class="badge-text">{{ role }}</p>
</NcBadge>
<NcBadge v-if="role === ProjectRoles.EDITOR" color="green">
<p class="badge-text">{{ role }}</p>
</NcBadge>
<NcBadge v-if="role === ProjectRoles.COMMENTER" color="orange">
<p class="badge-text">{{ role }}</p>
</NcBadge>
<NcBadge v-if="role === ProjectRoles.VIEWER" color="yellow">
<p class="badge-text">{{ role }}</p>
</NcBadge>
</a-select-option>
</NcSelect>
<RolesSelector
class="px-1"
:role="inviteData.roles"
:roles="allowedRoles"
:on-role-change="(role: ProjectRoles) => (inviteData.roles = role)"
:description="true"
/>
<a-button
type="primary"

74
packages/nc-gui/components/roles/Badge.vue

@ -0,0 +1,74 @@
<script lang="ts" setup>
import { RoleColors, RoleIcons, RoleLabels } from 'nocodb-sdk'
import { toRef } from '#imports'
const props = withDefaults(
defineProps<{
role: keyof typeof RoleLabels
clickable?: boolean
inherit?: boolean
border?: boolean
}>(),
{
clickable: false,
inherit: false,
border: true,
},
)
const roleRef = toRef(props, 'role')
const clickableRef = toRef(props, 'clickable')
const inheritRef = toRef(props, 'inherit')
const borderRef = toRef(props, 'border')
const roleProperties = computed(() => {
const role = roleRef.value
const color = RoleColors[role]
const icon = RoleIcons[role]
const label = RoleLabels[role]
return {
color,
icon,
label,
}
})
</script>
<template>
<div
class="flex items-center !border-0"
:class="{
'cursor-pointer': clickableRef,
}"
style="width: fit-content"
>
<NcBadge class="!h-auto !px-[8px]" :color="roleProperties.color" :border="borderRef">
<div
class="badge-text flex items-center gap-[4px]"
:class="{
'text-purple-500': roleProperties.color === 'purple',
'text-blue-500': roleProperties.color === 'blue',
'text-green-500': roleProperties.color === 'green',
'text-orange-500': roleProperties.color === 'orange',
'text-yellow-500': roleProperties.color === 'yellow',
'text-red-500': roleProperties.color === 'red',
'text-gray-300': !roleProperties.color,
}"
>
<GeneralIcon :icon="roleProperties.icon" />
{{ roleProperties.label }}
<GeneralIcon v-if="clickableRef" icon="arrowDown" />
</div>
</NcBadge>
<div class="flex-1"></div>
<!--
<a-tooltip v-if="inheritRef" placement="bottom">
<div class="text-gray-400 text-xs p-1 rounded-md">Workspace Role</div>
</a-tooltip>
-->
</div>
</template>
<style scoped lang="scss"></style>

57
packages/nc-gui/components/roles/Selector.vue

@ -0,0 +1,57 @@
<script lang="ts" setup>
import { RoleDescriptions } from 'nocodb-sdk'
import type { RoleLabels } from 'nocodb-sdk'
import { toRef } from '#imports'
const props = withDefaults(
defineProps<{
role: keyof typeof RoleLabels
roles: (keyof typeof RoleLabels)[]
description?: boolean
inherit?: string
onRoleChange: (role: keyof typeof RoleLabels) => void
}>(),
{
description: true,
},
)
const roleRef = toRef(props, 'role')
const inheritRef = toRef(props, 'inherit')
const descriptionRef = toRef(props, 'description')
</script>
<template>
<NcDropdown>
<RolesBadge class="border-1" data-testid="roles" :role="roleRef" :inherit="inheritRef === role" clickable />
<template #overlay>
<div class="nc-role-select-dropdown flex flex-col gap-[4px] p-1">
<div class="flex flex-col gap-[4px]">
<div v-for="rl in props.roles" :key="rl" :value="rl" :selected="rl === roleRef" @click="props.onRoleChange(rl)">
<div
class="flex flex-col py-[3px] px-[8px] gap-[4px] bg-transparent cursor-pointer"
:class="{
'w-[350px]': descriptionRef,
'w-[200px]': !descriptionRef,
}"
>
<div class="flex items-center justify-between">
<RolesBadge
class="!bg-white hover:!bg-gray-100"
:class="`nc-role-select-${rl}`"
:role="rl"
:inherit="inheritRef === rl"
:border="false"
/>
<GeneralIcon v-if="rl === roleRef" icon="check" />
</div>
<div v-if="descriptionRef" class="text-gray-500">{{ RoleDescriptions[rl] }}</div>
</div>
</div>
</div>
</div>
</template>
</NcDropdown>
</template>
<style scoped lang="scss"></style>

71
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -1,14 +1,14 @@
<script lang="ts" setup>
import { OrderedWorkspaceRoles, RoleColors, RoleLabels, WorkspaceUserRoles } from 'nocodb-sdk'
import { OrderedWorkspaceRoles, WorkspaceUserRoles } from 'nocodb-sdk'
import { storeToRefs, stringToColour, timeAgo, useWorkspace } from '#imports'
const { user } = useGlobal()
const { workspaceRoles, loadRoles } = useRoles()
const workspaceStore = useWorkspace()
const { removeCollaborator, updateCollaborator: _updateCollaborator } = workspaceStore
const { collaborators, workspaceRole } = storeToRefs(workspaceStore)
const { collaborators } = storeToRefs(workspaceStore)
const userSearchText = ref('')
const filterCollaborators = computed(() => {
@ -19,7 +19,8 @@ const filterCollaborators = computed(() => {
return collaborators.value.filter((collab) => collab.email!.includes(userSearchText.value))
})
const updateCollaborator = async (collab) => {
const updateCollaborator = async (collab: any, roles: WorkspaceUserRoles) => {
collab.roles = roles
try {
await _updateCollaborator(collab.id, collab.roles)
message.success('Successfully updated user role')
@ -29,14 +30,21 @@ const updateCollaborator = async (collab) => {
}
const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
const currentRoleIndex = OrderedWorkspaceRoles.findIndex((role) => role === workspaceRole.value)
return OrderedWorkspaceRoles.slice(currentRoleIndex + 1)
const currentRoleIndex = OrderedWorkspaceRoles.findIndex(
(role) => workspaceRoles.value && Object.keys(workspaceRoles.value).includes(role),
)
if (currentRoleIndex === -1) return []
return OrderedWorkspaceRoles.slice(currentRoleIndex + 1).filter((r) => r)
})
onMounted(async () => {
await loadRoles()
})
</script>
<template>
<div class="nc-collaborator-table-container mt-4 mx-6">
<WorkspaceInviteSection v-if="workspaceRole !== WorkspaceUserRoles.VIEWER" />
<WorkspaceInviteSection v-if="workspaceRoles !== WorkspaceUserRoles.VIEWER" />
<div class="w-full h-1 border-t-1 border-gray-100 opacity-50 mt-6"></div>
<div class="w-full flex flex-row justify-between items-baseline mt-6.5 mb-2 pr-0.25 ml-2">
<div class="text-xl">Collaborators</div>
@ -74,47 +82,18 @@ const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
{{ timeAgo(collab.created_at) }}
</td>
<td class="w-1/5 roles">
<div v-if="collab.roles === WorkspaceUserRoles.OWNER" class="nc-collaborator-role-select">
<a-select v-model:value="collab.roles" class="w-30 !rounded px-1" disabled>
<template #suffixIcon>
<MdiChevronDown />
</template>
<a-select-option :value="WorkspaceUserRoles.OWNER">
<NcBadge color="purple">
<p class="badge-text">Owner</p>
</NcBadge>
</a-select-option>
</a-select>
</div>
<div v-else class="nc-collaborator-role-select">
<NcSelect
v-model:value="collab.roles"
class="w-30 !rounded px-1"
:disabled="collab.id === user?.id || !accessibleRoles.includes(collab.roles)"
@change="updateCollaborator(collab)"
>
<template #suffixIcon>
<MdiChevronDown />
<div class="nc-collaborator-role-select">
<template v-if="accessibleRoles.includes(collab.roles)">
<RolesSelector
:role="collab.roles"
:roles="accessibleRoles"
:description="false"
:on-role-change="(role: WorkspaceUserRoles) => updateCollaborator(collab, role)"
/>
</template>
<a-select-option v-if="collab.id === user?.id" :value="workspaceRole">
<NcBadge :color="RoleColors[workspaceRole]">
<p class="badge-text">{{ RoleLabels[workspaceRole] }}</p>
</NcBadge>
</a-select-option>
<a-select-option v-if="!accessibleRoles.includes(collab.roles)" :value="collab.roles">
<NcBadge :color="RoleColors[collab.roles]">
<p class="badge-text">{{ RoleLabels[collab.roles] }}</p>
</NcBadge>
</a-select-option>
<template v-for="role of accessibleRoles" :key="`role-option-${role}`">
<a-select-option v-if="role" :value="role">
<NcBadge :color="RoleColors[role]">
<p class="badge-text">{{ RoleLabels[role] }}</p>
</NcBadge>
</a-select-option>
<template v-else>
<RolesBadge class="!bg-white" :role="collab.roles" />
</template>
</NcSelect>
</div>
</td>
<td class="w-1/5">

38
packages/nc-gui/components/workspace/InviteSection.vue

@ -10,7 +10,8 @@ const inviteData = reactive({
const workspaceStore = useWorkspace()
const { inviteCollaborator: _inviteCollaborator } = workspaceStore
const { isInvitingCollaborators, workspaceRole } = storeToRefs(workspaceStore)
const { isInvitingCollaborators } = storeToRefs(workspaceStore)
const { workspaceRoles } = useRoles()
const inviteCollaborator = async () => {
try {
@ -23,9 +24,19 @@ const inviteCollaborator = async () => {
}
// allow only lower roles to be assigned
const allowedRoles = computed<WorkspaceUserRoles[]>(() => {
const currentRoleIndex = OrderedWorkspaceRoles.findIndex((role) => role === workspaceRole.value)
return OrderedWorkspaceRoles.slice(currentRoleIndex + 1)
const allowedRoles = ref<WorkspaceUserRoles[]>([])
onMounted(async () => {
try {
const currentRoleIndex = OrderedWorkspaceRoles.findIndex(
(role) => workspaceRoles.value && Object.keys(workspaceRoles.value).includes(role),
)
if (currentRoleIndex !== -1) {
allowedRoles.value = OrderedWorkspaceRoles.slice(currentRoleIndex + 1).filter((r) => r)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
})
</script>
@ -41,18 +52,13 @@ const allowedRoles = computed<WorkspaceUserRoles[]>(() => {
class="!max-w-130 !rounded"
/>
<NcSelect v-model:value="inviteData.roles" class="min-w-30 !rounded px-1" data-testid="roles">
<template #suffixIcon>
<MdiChevronDown />
</template>
<template v-for="role of allowedRoles" :key="`role-option-${role}`">
<a-select-option v-if="role" :value="role">
<NcBadge :color="RoleColors[role]">
<p class="badge-text">{{ RoleLabels[role] }}</p>
</NcBadge>
</a-select-option>
</template>
</NcSelect>
<RolesSelector
class="px-1"
:role="inviteData.roles"
:roles="allowedRoles"
:on-role-change="(role: WorkspaceUserRoles) => (inviteData.roles = role)"
:description="true"
/>
<a-button
type="primary"

11
packages/nc-gui/components/workspace/View.vue

@ -1,13 +1,14 @@
<script lang="ts" setup>
import { useTitle } from '@vueuse/core'
import type { WorkspaceType } from 'nocodb-sdk'
import { isEeUI } from '#imports'
const router = useRouter()
const route = router.currentRoute
const { isUIAllowed } = useRoles()
const workspaceStore = useWorkspace()
const { isWorkspaceOwnerOrCreator, isWorkspaceOwner, activeWorkspace, workspaces } = storeToRefs(workspaceStore)
const { activeWorkspace, workspaces } = storeToRefs(workspaceStore)
const { loadCollaborators } = workspaceStore
const tab = computed({
@ -61,7 +62,7 @@ onMounted(() => {
</div>
<NcTabs v-model:activeKey="tab">
<template v-if="isWorkspaceOwnerOrCreator">
<template v-if="isUIAllowed('workspaceSettings')">
<a-tab-pane key="collaborators" class="w-full">
<template #tab>
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5">
@ -73,7 +74,7 @@ onMounted(() => {
</a-tab-pane>
</template>
<template v-if="isWorkspaceOwner && isEeUI">
<template v-if="isUIAllowed('workspaceBilling')">
<a-tab-pane key="billing" class="w-full">
<template #tab>
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5">
@ -84,7 +85,7 @@ onMounted(() => {
<WorkspaceBilling />
</a-tab-pane>
</template>
<template v-if="isWorkspaceOwner && isEeUI">
<template v-if="isUIAllowed('workspaceManage')">
<a-tab-pane key="settings" class="w-full">
<template #tab>
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-settings">

8
packages/nc-gui/composables/useRoles/index.ts

@ -111,6 +111,14 @@ export const useRoles = createSharedComposable(() => {
} else if (projectId) {
const res = await api.auth.me({ project_id: projectId })
user.value = {
...user.value,
roles: res.roles,
project_roles: res.project_roles,
} as typeof User
} else {
const res = await api.auth.me({})
user.value = {
...user.value,
roles: res.roles,

6
packages/nc-gui/middleware/auth.global.ts

@ -34,7 +34,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
const { api } = useApi({ useGlobalInstance: true })
const { allRoles } = useRoles()
const { allRoles, loadRoles } = useRoles()
/** If baseHostname defined block home page access under subdomains, and redirect to workspace page */
if (
@ -92,9 +92,9 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
/** if users are accessing the projects without having enough permissions, redirect to My Projects page */
if (to.params.projectId && from.params.projectId !== to.params.projectId) {
const user = await api.auth.me({ project_id: to.params.projectId as string })
await loadRoles()
if (user?.roles?.guest) {
if (state.user.value?.roles?.guest) {
message.error("You don't have enough permission to access the project.")
return navigateTo('/')

13
packages/nc-gui/store/users.ts

@ -3,6 +3,7 @@ import { acceptHMRUpdate, defineStore } from 'pinia'
export const useUsers = defineStore('userStore', () => {
const { api } = useApi()
const { user } = useGlobal()
const { loadRoles } = useRoles()
const updateUserProfile = async ({
attrs,
@ -21,17 +22,7 @@ export const useUsers = defineStore('userStore', () => {
}
}
const loadCurrentUser = async () => {
const res = await api.auth.me()
user.value = {
...user.value,
...res,
roles: res.roles,
project_roles: res.project_roles,
workspace_roles: res.workspace_roles,
}
}
const loadCurrentUser = loadRoles
watch(
() => user.value?.id,

19
packages/nc-gui/store/workspace.ts

@ -64,22 +64,6 @@ export const useWorkspace = defineStore('workspaceStore', () => {
}
})
/** getters */
const isWorkspaceCreator = computed(() => {
// todo: type correction
return orgRoles.value?.[OrgUserRoles.CREATOR]
})
const isWorkspaceOwner = computed(() => {
// todo: type correction
return orgRoles.value?.[OrgUserRoles.CREATOR]
})
const isWorkspaceOwnerOrCreator = computed(() => {
// todo: type correction
return orgRoles.value?.[OrgUserRoles.CREATOR]
})
/** actions */
const loadWorkspaces = async (_ignoreError = false) => {}
@ -239,8 +223,6 @@ export const useWorkspace = defineStore('workspaceStore', () => {
removeCollaborator,
updateCollaborator,
collaborators,
isWorkspaceCreator,
isWorkspaceOwner,
isInvitingCollaborators,
isCollaboratorsLoading,
addToFavourite,
@ -257,7 +239,6 @@ export const useWorkspace = defineStore('workspaceStore', () => {
clearWorkspaces,
upgradeActiveWorkspace,
navigateToWorkspace,
isWorkspaceOwnerOrCreator,
setLoadingState,
navigateToWorkspaceSettings,
lastPopulatedWorkspaceId,

13
packages/nc-gui/utils/iconUtils.ts

@ -76,6 +76,13 @@ import Up from '~icons/material-symbols/keyboard-arrow-up-rounded'
import Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill'
// Roles
import MaterialSymbolsManageAccountsOutline from '~icons/material-symbols/manage-accounts-outline'
// account
import MdiCommentAccountOutline from '~icons/mdi/comment-account-outline'
import MaterialSymbolsPersonSearchOutline from '~icons/material-symbols/person-search-outline'
import MaterialSymbolsBlock from '~icons/material-symbols/block'
// keep it for reference
// todo: remove it after all icons are migrated
/* export const iconMapOld = {
@ -400,6 +407,12 @@ export const iconMap = {
magic1: MdiMagicStaff,
workspace: h('span', { class: 'material-symbols' }, 'dataset'),
notification: NcNotification,
role_owner: MaterialSymbolsManageAccountsOutline,
role_creator: MaterialSymbolsManageAccountsOutline,
role_editor: h('span', { class: 'material-symbols' }, 'person'),
role_commenter: MdiCommentAccountOutline,
role_viewer: MaterialSymbolsPersonSearchOutline,
role_no_access: MaterialSymbolsBlock,
}
export const getMdiIcon = (type: string): any => {

42
packages/nocodb-sdk/src/lib/enums.ts

@ -194,6 +194,48 @@ export const RoleColors = {
[OrgUserRoles.VIEWER]: 'yellow',
};
export const RoleDescriptions = {
[WorkspaceUserRoles.OWNER]: 'Full access to workspace',
[WorkspaceUserRoles.CREATOR]: 'Can create projects, sync tables, views, setup web-hooks and more',
[WorkspaceUserRoles.EDITOR]: 'Can edit data in workspace projects',
[WorkspaceUserRoles.COMMENTER]: 'Can view and comment data in workspace projects',
[WorkspaceUserRoles.VIEWER]: 'Can view data in workspace projects',
[ProjectRoles.OWNER]: 'Full access to project',
[ProjectRoles.CREATOR]: 'Can create tables, views, setup webhook, invite collaborators and more',
[ProjectRoles.EDITOR]: 'Can view, add & modify records, add comments on them',
[ProjectRoles.COMMENTER]: 'Can view records and add comment on them',
[ProjectRoles.VIEWER]: 'Can only view records',
[ProjectRoles.NO_ACCESS]: 'Cannot access this project',
[OrgUserRoles.SUPER_ADMIN]: 'Full access to all',
[OrgUserRoles.CREATOR]: 'Can create projects, sync tables, views, setup web-hooks and more',
[OrgUserRoles.VIEWER]: 'Can only view projects',
};
export const RoleIcons = {
[WorkspaceUserRoles.OWNER]: 'role_owner',
[WorkspaceUserRoles.CREATOR]: 'role_creator',
[WorkspaceUserRoles.EDITOR]: 'role_editor',
[WorkspaceUserRoles.COMMENTER]: 'role_commenter',
[WorkspaceUserRoles.VIEWER]: 'role_viewer',
[ProjectRoles.OWNER]: 'role_owner',
[ProjectRoles.CREATOR]: 'role_creator',
[ProjectRoles.EDITOR]: 'role_editor',
[ProjectRoles.COMMENTER]: 'role_commenter',
[ProjectRoles.VIEWER]: 'role_viewer',
[ProjectRoles.NO_ACCESS]: 'role_no_access',
[OrgUserRoles.SUPER_ADMIN]: 'role_owner',
[OrgUserRoles.CREATOR]: 'role_creator',
[OrgUserRoles.VIEWER]: 'role_viewer',
};
export const WorkspaceRolesToProjectRoles = {
[WorkspaceUserRoles.OWNER]: ProjectRoles.OWNER,
[WorkspaceUserRoles.CREATOR]: ProjectRoles.CREATOR,
[WorkspaceUserRoles.EDITOR]: ProjectRoles.EDITOR,
[WorkspaceUserRoles.COMMENTER]: ProjectRoles.COMMENTER,
[WorkspaceUserRoles.VIEWER]: ProjectRoles.VIEWER,
};
export const OrderedWorkspaceRoles = [
WorkspaceUserRoles.OWNER,
WorkspaceUserRoles.CREATOR,

2
packages/nocodb/src/guards/global/global.guard.ts

@ -51,7 +51,7 @@ export class GlobalGuard extends AuthGuard(['jwt']) {
return this.authenticate(req, {
...req.user,
isAuthorized: true,
roles: req.user.roles === 'owner' ? 'owner,creator' : req.user.roles,
roles: req.user.roles,
});
}
} else if (req.headers['xc-shared-base-id']) {

3
packages/nocodb/src/models/User.ts

@ -263,8 +263,7 @@ export default class User implements UserType {
const projectRoles = await new Promise((resolve) => {
if (args.projectId) {
ProjectUser.get(args.projectId, user.id).then(async (projectUser) => {
let roles = projectUser?.roles;
roles = roles === 'owner' ? 'owner,creator' : roles;
const roles = projectUser?.roles;
// + (user.roles ? `,${user.roles}` : '');
if (roles) {
resolve(extractRolesObj(roles));

4
packages/nocodb/src/services/project-users/project-users.service.ts

@ -290,7 +290,7 @@ export class ProjectUsersService {
NcError.badRequest("Admin can't delete themselves!");
}
if (!param.req.user?.roles?.owner) {
if (!param.req.user?.project_roles?.owner) {
const user = await User.get(param.userId);
if (user.roles?.split(',').includes('super'))
NcError.forbidden(
@ -298,7 +298,7 @@ export class ProjectUsersService {
);
const projectUser = await ProjectUser.get(project_id, param.userId);
if (projectUser?.roles?.split(',').includes('super'))
if (projectUser?.roles?.split(',').includes('owner'))
NcError.forbidden('Insufficient privilege to delete a owner user.');
}

2
packages/nocodb/src/strategies/google.strategy/google.strategy.ts

@ -36,8 +36,6 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
ProjectUser.get(req.ncProjectId, user.id)
.then(async (projectUser) => {
user.roles = projectUser?.roles || user.roles;
user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : '');
done(null, sanitiseUserObj(user));

2
packages/nocodb/src/utils/roleHelper.ts

@ -6,7 +6,7 @@ export function getProjectRolePower(user: any) {
const reverseOrderedProjectRoles = [...OrderedProjectRoles].reverse();
if (!user.project_roles) {
NcError.badRequest('Role not found');
return -1;
}
// get most powerful role of user (TODO moving forward we will confirm that user has only one role)

6
tests/playwright/pages/Dashboard/ProjectView/AccessSettingsPage.ts

@ -22,11 +22,11 @@ export class AccessSettingsPage extends BasePage {
if (userEmail === email) {
const roleDropdown = user.locator('.nc-collaborator-role-select');
const selectedRole = await user.locator('.nc-collaborator-role-select').innerText();
const selectedRole = await user.locator('.nc-collaborator-role-select .badge-text').innerText();
await roleDropdown.click();
const menu = this.rootPage.locator('.ant-select-dropdown:visible');
const clickClbk = () => menu.locator(`.ant-select-item:has-text("${role}"):visible`).last().click();
const menu = this.rootPage.locator('.nc-role-select-dropdown:visible');
const clickClbk = () => menu.locator(`.nc-role-select-${role.toLowerCase()}:visible`).last().click();
if (networkValidation && !selectedRole.includes(role)) {
await this.waitForResponse({

4
tests/playwright/pages/WorkspacePage/CollaborationPage.ts

@ -39,8 +39,8 @@ export class CollaborationPage extends BasePage {
// role
await this.selector_role.click();
await this.rootPage.waitForTimeout(500);
await this.rootPage.locator(`.ant-select-item-option-content:has-text("${role}"):visible`).click();
const menu = this.rootPage.locator('.nc-role-select-dropdown:visible');
await menu.locator(`.nc-role-select-workspace-level-${role.toLowerCase()}:visible`).click();
// submit

2
tests/playwright/setup/index.ts

@ -384,7 +384,7 @@ const setup = async ({
email: `user@nocodb.com`,
password: getDefaultPwd(),
});
await axios.post(`http://localhost:8080/api/v1/license`, { key: '' }, { headers: { 'xc-auth': admin.data.token } });
if (!isEE()) await axios.post(`http://localhost:8080/api/v1/license`, { key: '' }, { headers: { 'xc-auth': admin.data.token } });
} catch (e) {
// ignore error: some roles will not have permission for license reset
// console.error(`Error resetting project: ${process.env.TEST_PARALLEL_INDEX}`, e);

Loading…
Cancel
Save