Browse Source

feat: new role badge and selector

pull/6378/head
mertmit 1 year ago
parent
commit
ff2f61ee1b
  1. 1
      packages/nc-gui/components/nc/Badge.vue
  2. 110
      packages/nc-gui/components/project/AccessSettings.vue
  3. 75
      packages/nc-gui/components/roles/Badge.vue
  4. 38
      packages/nc-gui/components/roles/Selector.vue
  5. 13
      packages/nc-gui/utils/iconUtils.ts
  6. 34
      packages/nocodb-sdk/src/lib/enums.ts
  7. 2
      packages/nocodb/src/guards/global/global.guard.ts
  8. 3
      packages/nocodb/src/models/User.ts
  9. 2
      packages/nocodb/src/strategies/google.strategy/google.strategy.ts

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

@ -13,6 +13,7 @@ const props = defineProps<{
'border-green-500 bg-green-100': props.color === 'green', 'border-green-500 bg-green-100': props.color === 'green',
'border-orange-500 bg-orange-100': props.color === 'orange', 'border-orange-500 bg-orange-100': props.color === 'orange',
'border-yellow-500 bg-yellow-100': props.color === 'yellow', 'border-yellow-500 bg-yellow-100': props.color === 'yellow',
'border-red-500 bg-red-100': props.color === 'red',
'border-gray-300': !props.color, 'border-gray-300': !props.color,
}" }"
> >

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

@ -1,15 +1,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { WorkspaceUserType } from 'nocodb-sdk' import type { OrgUserRoles, WorkspaceUserRoles } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles, RoleColors, RoleLabels } from 'nocodb-sdk' import { OrderedProjectRoles, ProjectRoles, WorkspaceRolesToProjectRoles } from 'nocodb-sdk'
import InfiniteLoading from 'v3-infinite-loading' 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 projectsStore = useProjects()
const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = projectsStore const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = projectsStore
const { activeProjectId } = storeToRefs(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 totalCollaborators = ref(0)
const userSearchText = ref('') const userSearchText = ref('')
const currentPage = ref(0) const currentPage = ref(0)
@ -34,9 +44,12 @@ const loadCollaborators = async () => {
...collaborators.value, ...collaborators.value,
...users.map((user: any) => ({ ...users.map((user: any) => ({
...user, ...user,
projectRoles: user.roles, project_roles: user.roles,
// TODO: Remove this hack and make the values consistent with the backend roles:
roles: user.roles ?? (RoleLabels[user.workspace_roles as string] as string)?.toLowerCase() ?? ProjectRoles.NO_ACCESS, user.roles ??
(user.workspace_roles
? WorkspaceRolesToProjectRoles[user.workspace_roles as WorkspaceUserRoles] ?? ProjectRoles.NO_ACCESS
: ProjectRoles.NO_ACCESS),
})), })),
] ]
} catch (e: any) { } catch (e: any) {
@ -62,16 +75,29 @@ const loadListData = async ($state: any) => {
$state.loaded() $state.loaded()
} }
const updateCollaborator = async (collab, roles) => { const updateCollaborator = async (collab: any, roles: ProjectRoles) => {
try { 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) await removeProjectUser(activeProjectId.value!, collab)
collab.projectRoles = null if (
} else if (collab.projectRoles) { collab.workspace_roles &&
WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles] === roles &&
isEeUI
) {
collab.roles = WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles]
} else {
collab.roles = null
}
} else if (collab.project_roles) {
collab.roles = roles
await updateProjectUser(activeProjectId.value!, collab) await updateProjectUser(activeProjectId.value!, collab)
} else { } else {
collab.roles = roles
await createProjectUser(activeProjectId.value!, collab) await createProjectUser(activeProjectId.value!, collab)
collab.projectRoles = roles
} }
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
@ -107,16 +133,13 @@ const reloadCollabs = async () => {
await loadCollaborators() 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 () => { onMounted(async () => {
isLoading.value = true isLoading.value = true
try { try {
await loadCollaborators() 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) { if (currentRoleIndex !== -1) {
accessibleRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1) accessibleRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1)
} }
@ -180,41 +203,22 @@ onMounted(async () => {
{{ timeAgo(collab.created_at) }} {{ timeAgo(collab.created_at) }}
</div> </div>
<div class="w-1/5 roles"> <div class="w-1/5 roles">
<div class="nc-collaborator-role-select"> <div class="nc-collaborator-role-select p-2">
<NcSelect <template v-if="accessibleRoles.includes(collab.roles)">
v-model:value="collab.roles" <RolesSelector
class="w-35 !rounded px-1" :role="collab.roles"
:virtual="true" :roles="accessibleRoles"
:placeholder="$t('labels.noAccess')" :inherit="
:disabled="collab.id === user?.id || (collab.roles && !accessibleRoles.includes(collab.roles))" isEeUI && collab.workspace_roles && WorkspaceRolesToProjectRoles[collab.workspace_roles]
@change="(value) => updateCollaborator(collab, value)" ? WorkspaceRolesToProjectRoles[collab.workspace_roles]
> : null
<template #suffixIcon> "
<MdiChevronDown /> :on-role-change="(role: ProjectRoles) => updateCollaborator(collab, role)"
/>
</template> </template>
<a-select-option v-if="collab.id === user?.id" :value="userProjectRole"> <template v-else>
<NcBadge :color="RoleColors[userProjectRole]"> <RolesBadge class="!bg-white" :role="collab.roles" />
<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> </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> </div>
<div class="w-1/5"></div> <div class="w-1/5"></div>
@ -238,7 +242,7 @@ onMounted(async () => {
<style scoped lang="scss"> <style scoped lang="scss">
.badge-text { .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 { .nc-collaborators-list {

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

@ -0,0 +1,75 @@
<script lang="ts" setup>
import { RoleColors, RoleDescriptions, RoleIcons, RoleLabels } from 'nocodb-sdk'
import { toRef } from '#imports'
const props = withDefaults(
defineProps<{
role: keyof typeof RoleLabels
description?: boolean
clickable?: boolean
inherit?: boolean
}>(),
{
description: true,
clickable: false,
inherit: false,
},
)
const roleRef = toRef(props, 'role')
const descriptionRef = toRef(props, 'description')
const clickableRef = toRef(props, 'clickable')
const inheritRef = toRef(props, 'inherit')
const roleProperties = computed(() => {
const role = roleRef.value
const color = RoleColors[role]
const icon = RoleIcons[role]
const label = RoleLabels[role]
const descriptionText = RoleDescriptions[role]
return {
color,
icon,
label,
descriptionText,
}
})
</script>
<template>
<div
class="flex flex-col py-[6px] px-[8px] gap-[4px] w-[350px] rounded-md bg-gray-50"
:class="{
'cursor-pointer hover:bg-gray-100': clickableRef,
}"
>
<div class="flex items-center">
<NcBadge class="!h-auto !px-[8px]" :color="roleProperties.color">
<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 }}
</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">Current Workspace Role</div>
</a-tooltip>
</div>
<div v-if="descriptionRef" class="text-gray-500">{{ roleProperties.descriptionText }}</div>
</div>
</template>
<style scoped lang="scss"></style>

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

@ -0,0 +1,38 @@
<script lang="ts" setup>
import type { RoleLabels } from 'nocodb-sdk'
import { toRef } from '#imports'
const props = defineProps<{
role: keyof typeof RoleLabels
roles: (keyof typeof RoleLabels)[]
inherit?: string
onRoleChange: (role: keyof typeof RoleLabels) => void
}>()
const roleRef = toRef(props, 'role')
const inheritRef = toRef(props, 'inherit')
</script>
<template>
<NcDropdown>
<RolesBadge class="border-1" :role="roleRef" clickable :inherit="inheritRef === role" />
<template #overlay>
<div class="flex flex-col gap-[4px] p-1">
<div class="flex flex-col gap-[4px]">
<NcDropdownItem
v-for="rl in props.roles"
:key="rl"
class="cursor-pointer"
:value="rl"
:selected="rl === roleRef"
@click="props.onRoleChange(rl)"
>
<RolesBadge class="!bg-white hover:!bg-gray-100" :role="rl" :inherit="inheritRef === rl" />
</NcDropdownItem>
</div>
</div>
</template>
</NcDropdown>
</template>
<style scoped lang="scss"></style>

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 Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill' 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 // keep it for reference
// todo: remove it after all icons are migrated // todo: remove it after all icons are migrated
/* export const iconMapOld = { /* export const iconMapOld = {
@ -400,6 +407,12 @@ export const iconMap = {
magic1: MdiMagicStaff, magic1: MdiMagicStaff,
workspace: h('span', { class: 'material-symbols' }, 'dataset'), workspace: h('span', { class: 'material-symbols' }, 'dataset'),
notification: NcNotification, 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 => { export const getMdiIcon = (type: string): any => {

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

@ -194,6 +194,40 @@ export const RoleColors = {
[OrgUserRoles.VIEWER]: 'yellow', [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 web-hooks and more',
[ProjectRoles.EDITOR]: 'Can only edit data in project',
[ProjectRoles.COMMENTER]: 'Can only view and comment on data in project',
[ProjectRoles.VIEWER]: 'Can only view data in project',
[ProjectRoles.NO_ACCESS]: 'User cannot view 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 = { export const WorkspaceRolesToProjectRoles = {
[WorkspaceUserRoles.OWNER]: ProjectRoles.OWNER, [WorkspaceUserRoles.OWNER]: ProjectRoles.OWNER,
[WorkspaceUserRoles.CREATOR]: ProjectRoles.CREATOR, [WorkspaceUserRoles.CREATOR]: ProjectRoles.CREATOR,

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

@ -51,7 +51,7 @@ export class GlobalGuard extends AuthGuard(['jwt']) {
return this.authenticate(req, { return this.authenticate(req, {
...req.user, ...req.user,
isAuthorized: true, isAuthorized: true,
roles: req.user.roles === 'owner' ? 'owner,creator' : req.user.roles, roles: req.user.roles,
}); });
} }
} else if (req.headers['xc-shared-base-id']) { } 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) => { const projectRoles = await new Promise((resolve) => {
if (args.projectId) { if (args.projectId) {
ProjectUser.get(args.projectId, user.id).then(async (projectUser) => { ProjectUser.get(args.projectId, user.id).then(async (projectUser) => {
let roles = projectUser?.roles; const roles = projectUser?.roles;
roles = roles === 'owner' ? 'owner,creator' : roles;
// + (user.roles ? `,${user.roles}` : ''); // + (user.roles ? `,${user.roles}` : '');
if (roles) { if (roles) {
resolve(extractRolesObj(roles)); resolve(extractRolesObj(roles));

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) ProjectUser.get(req.ncProjectId, user.id)
.then(async (projectUser) => { .then(async (projectUser) => {
user.roles = projectUser?.roles || user.roles; user.roles = projectUser?.roles || user.roles;
user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : ''); // + (user.roles ? `,${user.roles}` : '');
done(null, sanitiseUserObj(user)); done(null, sanitiseUserObj(user));

Loading…
Cancel
Save