Browse Source

Merge pull request #6399 from nocodb/nc-fix/ui-acl

fix: revise frontend acl
pull/6427/head
mertmit 1 year ago committed by GitHub
parent
commit
5c3ed74fba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      packages/nc-gui/components/account/UserList.vue
  2. 10
      packages/nc-gui/components/account/UsersModal.vue
  3. 24
      packages/nc-gui/components/cell/MultiSelect.vue
  4. 23
      packages/nc-gui/components/cell/SingleSelect.vue
  5. 4
      packages/nc-gui/components/cell/TextArea.vue
  6. 13
      packages/nc-gui/components/cell/attachment/Modal.vue
  7. 16
      packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue
  8. 14
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  9. 32
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  10. 18
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  11. 6
      packages/nc-gui/components/dashboard/TreeView/index.vue
  12. 4
      packages/nc-gui/components/general/AddBaseButton.vue
  13. 9
      packages/nc-gui/components/general/PreviewAs.vue
  14. 4
      packages/nc-gui/components/general/ShareBaseButton.vue
  15. 6
      packages/nc-gui/components/general/ShareProject.vue
  16. 7
      packages/nc-gui/components/project/AllTables.vue
  17. 10
      packages/nc-gui/components/project/InviteProjectCollabSection.vue
  18. 6
      packages/nc-gui/components/project/View.vue
  19. 6
      packages/nc-gui/components/smartsheet/Cell.vue
  20. 2
      packages/nc-gui/components/smartsheet/Details.vue
  21. 8
      packages/nc-gui/components/smartsheet/Form.vue
  22. 4
      packages/nc-gui/components/smartsheet/Gallery.vue
  23. 4
      packages/nc-gui/components/smartsheet/Kanban.vue
  24. 4
      packages/nc-gui/components/smartsheet/Toolbar.vue
  25. 17
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  26. 12
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  27. 3
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  28. 18
      packages/nc-gui/components/smartsheet/grid/Table.vue
  29. 12
      packages/nc-gui/components/smartsheet/header/Cell.vue
  30. 6
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  31. 8
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  32. 6
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  33. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/Developer.vue
  34. 6
      packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue
  35. 6
      packages/nc-gui/components/smartsheet/toolbar/MoreActions.vue
  36. 6
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  37. 6
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  38. 4
      packages/nc-gui/components/tabs/Auth.vue
  39. 6
      packages/nc-gui/components/tabs/Smartsheet.vue
  40. 15
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  41. 19
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  42. 6
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  43. 6
      packages/nc-gui/components/virtual-cell/HasMany.vue
  44. 4
      packages/nc-gui/components/virtual-cell/Links.vue
  45. 6
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  46. 4
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  47. 4
      packages/nc-gui/components/workspace/CreateProjectBtn.vue
  48. 22
      packages/nc-gui/components/workspace/ProjectList.vue
  49. 2
      packages/nc-gui/composables/useExpandedFormStore.ts
  50. 5
      packages/nc-gui/composables/useGlobal/types.ts
  51. 17
      packages/nc-gui/composables/useGridViewColumn.ts
  52. 6
      packages/nc-gui/composables/useKanbanViewStore.ts
  53. 4
      packages/nc-gui/composables/useMapViewDataStore.ts
  54. 58
      packages/nc-gui/composables/useRoles/index.ts
  55. 61
      packages/nc-gui/composables/useUIPermission/index.ts
  56. 0
      packages/nc-gui/composables/useUIPermission/rolePermissions.ts
  57. 19
      packages/nc-gui/composables/useViewColumns.ts
  58. 6
      packages/nc-gui/composables/useViewData.ts
  59. 4
      packages/nc-gui/composables/useViewFilters.ts
  60. 2
      packages/nc-gui/composables/useViewGroupBy.ts
  61. 4
      packages/nc-gui/composables/useViewSorts.ts
  62. 2
      packages/nc-gui/layouts/base.vue
  63. 2
      packages/nc-gui/layouts/new.vue
  64. 148
      packages/nc-gui/lib/acl.ts
  65. 89
      packages/nc-gui/lib/constants.ts
  66. 21
      packages/nc-gui/lib/enums.ts
  67. 1
      packages/nc-gui/lib/index.ts
  68. 31
      packages/nc-gui/lib/types.ts
  69. 6
      packages/nc-gui/pages/account/index.vue
  70. 4
      packages/nc-gui/pages/account/index/users/[[nestedPage]].vue
  71. 4
      packages/nc-gui/store/projectsShortcuts.ts
  72. 5
      packages/nc-gui/store/share.ts
  73. 7
      packages/nc-gui/store/workspace.ts
  74. 1
      packages/nc-gui/utils/index.ts
  75. 15
      packages/nc-gui/utils/userUtils.ts
  76. 10
      packages/nocodb-sdk/src/lib/globals.ts
  77. 12
      packages/nocodb-sdk/src/lib/helperFunctions.ts

11
packages/nc-gui/components/account/UserList.vue

@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { OrgUserReqType, RequestParams, UserType } from 'nocodb-sdk' import { OrgUserRoles } from 'nocodb-sdk'
import type { OrgUserReqType, RequestParams, Roles, UserType } from 'nocodb-sdk'
import type { User } from '#imports' import type { User } from '#imports'
import { Role, extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useNuxtApp } from '#imports' import { extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useNuxtApp } from '#imports'
const { api, isLoading } = useApi() const { api, isLoading } = useApi()
@ -58,7 +59,7 @@ const loadUsers = async (page = currentPage.value, limit = currentLimit.value) =
loadUsers() loadUsers()
const updateRole = async (userId: string, roles: Role) => { const updateRole = async (userId: string, roles: Roles) => {
try { try {
await api.orgUsers.update(userId, { await api.orgUsers.update(userId, {
roles, roles,
@ -195,7 +196,7 @@ const copyPasswordResetUrl = async (user: User) => {
> >
<a-select-option <a-select-option
class="nc-users-list-role-option" class="nc-users-list-role-option"
:value="Role.OrgLevelCreator" :value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)" :label="$t(`objects.roleType.orgLevelCreator`)"
> >
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div> <div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
@ -206,7 +207,7 @@ const copyPasswordResetUrl = async (user: User) => {
<a-select-option <a-select-option
class="nc-users-list-role-option" class="nc-users-list-role-option"
:value="Role.OrgLevelViewer" :value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)" :label="$t(`objects.roleType.orgLevelViewer`)"
> >
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div> <div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>

10
packages/nc-gui/components/account/UsersModal.vue

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import type { OrgUserReqType } from 'nocodb-sdk' import type { OrgUserReqType } from 'nocodb-sdk'
import { OrgUserRoles } from 'nocodb-sdk'
import type { User, Users } from '#imports' import type { User, Users } from '#imports'
import { import {
Form, Form,
Role,
computed, computed,
emailValidator, emailValidator,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
@ -34,7 +34,7 @@ const { copy } = useCopy()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
const usersData = ref<Users>({ emails: '', role: Role.OrgLevelViewer, invitationToken: undefined }) const usersData = ref<Users>({ emails: '', role: OrgUserRoles.VIEWER, invitationToken: undefined })
const formRef = ref() const formRef = ref()
@ -90,7 +90,7 @@ const copyUrl = async () => {
const clickInviteMore = () => { const clickInviteMore = () => {
$e('c:user:invite-more') $e('c:user:invite-more')
usersData.value.invitationToken = undefined usersData.value.invitationToken = undefined
usersData.value.role = Role.OrgLevelViewer usersData.value.role = OrgUserRoles.VIEWER
usersData.value.emails = '' usersData.value.emails = ''
} }
@ -196,7 +196,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role"> <a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role">
<a-select-option <a-select-option
class="nc-role-option" class="nc-role-option"
:value="Role.OrgLevelCreator" :value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)" :label="$t(`objects.roleType.orgLevelCreator`)"
> >
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div> <div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
@ -207,7 +207,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<a-select-option <a-select-option
class="nc-role-option" class="nc-role-option"
:value="Role.OrgLevelViewer" :value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)" :label="$t(`objects.roleType.orgLevelViewer`)"
> >
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div> <div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>

24
packages/nc-gui/components/cell/MultiSelect.vue

@ -4,7 +4,6 @@ import { message } from 'ant-design-vue'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import type { Select as AntSelect } from 'ant-design-vue' import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType, SelectOptionsType } from 'nocodb-sdk' import type { SelectOptionType, SelectOptionsType } from 'nocodb-sdk'
import { WorkspaceUserRoles } from 'nocodb-sdk'
import { import {
ActiveCellInj, ActiveCellInj,
CellClickHookInj, CellClickHookInj,
@ -80,7 +79,7 @@ const { $api } = useNuxtApp()
const { getMeta } = useMetas() const { getMeta } = useMetas()
const { hasRole } = useRoles() const { isUIAllowed } = useRoles()
const { isPg, isMysql } = useProject() const { isPg, isMysql } = useProject()
@ -105,15 +104,7 @@ const isOptionMissing = computed(() => {
return (options.value ?? []).every((op) => op.title !== searchVal.value) return (options.value ?? []).every((op) => op.title !== searchVal.value)
}) })
const hasEditRoles = computed( const hasEditRoles = computed(() => isUIAllowed('dataEdit'))
() =>
hasRole('owner', true) ||
hasRole('creator', true) ||
hasRole('editor', true) ||
hasRole(WorkspaceUserRoles.OWNER, true) ||
hasRole(WorkspaceUserRoles.CREATOR, true) ||
hasRole(WorkspaceUserRoles.EDITOR, true),
)
const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value) const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value)
@ -423,16 +414,7 @@ const selectedOpts = computed(() => {
</a-select-option> </a-select-option>
<a-select-option <a-select-option
v-if=" v-if="searchVal && isOptionMissing && !isPublic && !disableOptionCreation && isUIAllowed('fieldEdit')"
searchVal &&
isOptionMissing &&
!isPublic &&
!disableOptionCreation &&
(hasRole('owner', true) ||
hasRole('creator', true) ||
hasRole(WorkspaceUserRoles.OWNER, true) ||
hasRole(WorkspaceUserRoles.CREATOR, true))
"
:key="searchVal" :key="searchVal"
:value="searchVal" :value="searchVal"
> >

23
packages/nc-gui/components/cell/SingleSelect.vue

@ -4,7 +4,6 @@ import { message } from 'ant-design-vue'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import type { Select as AntSelect } from 'ant-design-vue' import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk' import type { SelectOptionType } from 'nocodb-sdk'
import { WorkspaceUserRoles } from 'nocodb-sdk'
import { import {
ActiveCellInj, ActiveCellInj,
CellClickHookInj, CellClickHookInj,
@ -70,7 +69,7 @@ const searchVal = ref()
const { getMeta } = useMetas() const { getMeta } = useMetas()
const { hasRole } = useRoles() const { isUIAllowed } = useRoles()
const { isPg, isMysql } = useProject() const { isPg, isMysql } = useProject()
@ -78,15 +77,7 @@ const { isPg, isMysql } = useProject()
// temporary until it's add the option to column meta // temporary until it's add the option to column meta
const tempSelectedOptState = ref<string>() const tempSelectedOptState = ref<string>()
const isNewOptionCreateEnabled = computed( const isNewOptionCreateEnabled = computed(() => !isPublic.value && !disableOptionCreation && isUIAllowed('fieldEdit'))
() =>
!isPublic.value &&
!disableOptionCreation &&
(hasRole('owner', true) ||
hasRole('creator', true) ||
hasRole(WorkspaceUserRoles.OWNER, true) ||
hasRole(WorkspaceUserRoles.CREATOR, true)),
)
const options = computed<(SelectOptionType & { value: string })[]>(() => { const options = computed<(SelectOptionType & { value: string })[]>(() => {
if (column?.value.colOptions) { if (column?.value.colOptions) {
@ -106,15 +97,7 @@ const isOptionMissing = computed(() => {
return (options.value ?? []).every((op) => op.title !== searchVal.value) return (options.value ?? []).every((op) => op.title !== searchVal.value)
}) })
const hasEditRoles = computed( const hasEditRoles = computed(() => isUIAllowed('dataEdit'))
() =>
hasRole('owner', true) ||
hasRole('creator', true) ||
hasRole('editor', true) ||
hasRole(WorkspaceUserRoles.OWNER, true) ||
hasRole(WorkspaceUserRoles.CREATOR, true) ||
hasRole(WorkspaceUserRoles.EDITOR, true),
)
const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value) const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value)

4
packages/nc-gui/components/cell/TextArea.vue

@ -6,11 +6,11 @@ import {
EditColumnInj, EditColumnInj,
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
ReadonlyInj,
RowHeightInj, RowHeightInj,
iconMap, iconMap,
inject, inject,
useVModel, useVModel,
ReadonlyInj,
} from '#imports' } from '#imports'
const props = defineProps<{ const props = defineProps<{
@ -139,9 +139,9 @@ onClickOutside(inputWrapperRef, (e) => {
class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black" class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black"
:bordered="false" :bordered="false"
:auto-size="{ minRows: 20, maxRows: 20 }" :auto-size="{ minRows: 20, maxRows: 20 }"
:disabled="readOnly"
@keydown.stop @keydown.stop
@keydown.escape="isVisible = false" @keydown.escape="isVisible = false"
:disabled="readOnly"
/> />
</div> </div>
</template> </template>

13
packages/nc-gui/components/cell/attachment/Modal.vue

@ -2,9 +2,9 @@
import { onKeyDown, useEventListener } from '@vueuse/core' import { onKeyDown, useEventListener } from '@vueuse/core'
import { useAttachmentCell } from './utils' import { useAttachmentCell } from './utils'
import { useSortable } from './sort' import { useSortable } from './sort'
import { iconMap, isImage, ref, useAttachment, useDropZone, useUIPermission, watch } from '#imports' import { iconMap, isImage, ref, useAttachment, useDropZone, useRoles, watch } from '#imports'
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { const {
open, open,
@ -96,7 +96,7 @@ const handleFileDelete = (i: number) => {
<template #title> <template #title>
<div class="flex gap-4"> <div class="flex gap-4">
<div <div
v-if="isSharedForm || (!readOnly && isUIAllowed('tableAttachment') && !isPublic && !isLocked)" v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic && !isLocked)"
class="nc-attach-file group" class="nc-attach-file group"
data-testid="attachment-expand-file-picker-button" data-testid="attachment-expand-file-picker-button"
@click="open" @click="open"
@ -141,7 +141,7 @@ const handleFileDelete = (i: number) => {
<template #title> Remove File </template> <template #title> Remove File </template>
<component <component
:is="iconMap.closeCircle" :is="iconMap.closeCircle"
v-if="isSharedForm || (isUIAllowed('tableAttachment') && !isPublic && !isLocked)" v-if="isSharedForm || (isUIAllowed('dataEdit') && !isPublic && !isLocked)"
class="nc-attachment-remove" class="nc-attachment-remove"
@click.stop="onRemoveFileClick(item.title, i)" @click.stop="onRemoveFileClick(item.title, i)"
/> />
@ -155,10 +155,7 @@ const handleFileDelete = (i: number) => {
</div> </div>
</a-tooltip> </a-tooltip>
<a-tooltip <a-tooltip v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic && !isLocked)" placement="bottom">
v-if="isSharedForm || (!readOnly && isUIAllowed('tableAttachment') && !isPublic && !isLocked)"
placement="bottom"
>
<template #title> Rename File </template> <template #title> Rename File </template>
<div class="nc-attachment-download group-hover:(opacity-100) mr-[35px]"> <div class="nc-attachment-download group-hover:(opacity-100) mr-[35px]">

16
packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue

@ -4,7 +4,7 @@ import { storeToRefs } from 'pinia'
import { toRef } from '@vue/reactivity' import { toRef } from '@vue/reactivity'
import { resolveComponent } from '@vue/runtime-core' import { resolveComponent } from '@vue/runtime-core'
import { ref } from 'vue' import { ref } from 'vue'
import { ProjectRoleInj, useDialog, useUIPermission } from '#imports' import { ProjectRoleInj, useDialog, useRoles } from '#imports'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -20,7 +20,7 @@ const emit = defineEmits<{
openTableCreateDialog: () => void openTableCreateDialog: () => void
}>() }>()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const project = toRef(props, 'project') const project = toRef(props, 'project')
@ -116,7 +116,7 @@ function openTableCreateMagicDialog(baseId?: string) {
<template> <template>
<div <div
v-if="isUIAllowed('table-create', false, projectRole)" v-if="isUIAllowed('tableCreate', { roles: projectRole })"
class="group flex items-center gap-2 pl-2 pr-4.75 py-1 text-primary/70 hover:(text-primary/100) cursor-pointer select-none" class="group flex items-center gap-2 pl-2 pr-4.75 py-1 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="emit('openTableCreateDialog')" @click="emit('openTableCreateDialog')"
> >
@ -158,7 +158,7 @@ function openTableCreateMagicDialog(baseId?: string) {
<!-- Quick Import From --> <!-- Quick Import From -->
<a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0"> <a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0">
<a-menu-item <a-menu-item
v-if="isUIAllowed('airtableImport', false, projectRole)" v-if="isUIAllowed('airtableImport', { roles: projectRole })"
key="quick-import-airtable" key="quick-import-airtable"
@click="openAirtableImportDialog(project.bases[baseIndex].id)" @click="openAirtableImportDialog(project.bases[baseIndex].id)"
> >
@ -169,7 +169,7 @@ function openTableCreateMagicDialog(baseId?: string) {
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('csvImport', false, projectRole)" v-if="isUIAllowed('csvImport', { roles: projectRole })"
key="quick-import-csv" key="quick-import-csv"
@click="openQuickImportDialog('csv', project.bases[baseIndex].id)" @click="openQuickImportDialog('csv', project.bases[baseIndex].id)"
> >
@ -180,7 +180,7 @@ function openTableCreateMagicDialog(baseId?: string) {
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('jsonImport', false, projectRole)" v-if="isUIAllowed('jsonImport', { roles: projectRole })"
key="quick-import-json" key="quick-import-json"
@click="openQuickImportDialog('json', project.bases[baseIndex].id)" @click="openQuickImportDialog('json', project.bases[baseIndex].id)"
> >
@ -191,7 +191,7 @@ function openTableCreateMagicDialog(baseId?: string) {
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('excelImport', false, projectRole)" v-if="isUIAllowed('excelImport', { roles: projectRole })"
key="quick-import-excel" key="quick-import-excel"
@click="openQuickImportDialog('excel', project.bases[baseIndex].id)" @click="openQuickImportDialog('excel', project.bases[baseIndex].id)"
> >
@ -243,7 +243,7 @@ function openTableCreateMagicDialog(baseId?: string) {
<a-menu-divider class="my-0" /> --> <a-menu-divider class="my-0" /> -->
<a-menu-item v-if="isUIAllowed('importRequest', false, projectRole)" key="add-new-table" class="py-1 rounded-b"> <a-menu-item v-if="isUIAllowed('importRequest', { roles: projectRole })" key="add-new-table" class="py-1 rounded-b">
<a <a
v-e="['e:datasource:import-request']" v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052" href="https://github.com/nocodb/nocodb/issues/2052"

14
packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue

@ -8,7 +8,7 @@ const props = defineProps<{
const base = toRef(props, 'base') const base = toRef(props, 'base')
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const projectRole = inject(ProjectRoleInj) const projectRole = inject(ProjectRoleInj)
@ -68,7 +68,7 @@ function openQuickImportDialog(type: string) {
<template #expandIcon></template> <template #expandIcon></template>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('airtableImport', false, projectRole)" v-if="isUIAllowed('airtableImport', { roles: projectRole })"
key="quick-import-airtable" key="quick-import-airtable"
@click="openAirtableImportDialog(base.id)" @click="openAirtableImportDialog(base.id)"
> >
@ -76,13 +76,17 @@ function openQuickImportDialog(type: string) {
<div class="ml-0.5">Airtable</div> <div class="ml-0.5">Airtable</div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem v-if="isUIAllowed('csvImport', false, projectRole)" key="quick-import-csv" @click="openQuickImportDialog('csv')"> <NcMenuItem
v-if="isUIAllowed('csvImport', { roles: projectRole })"
key="quick-import-csv"
@click="openQuickImportDialog('csv')"
>
<GeneralIcon icon="csv" class="w-4 group-hover:text-black" /> <GeneralIcon icon="csv" class="w-4 group-hover:text-black" />
CSV file CSV file
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('jsonImport', false, projectRole)" v-if="isUIAllowed('jsonImport', { roles: projectRole })"
key="quick-import-json" key="quick-import-json"
@click="openQuickImportDialog('json')" @click="openQuickImportDialog('json')"
> >
@ -91,7 +95,7 @@ function openQuickImportDialog(type: string) {
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('excelImport', false, projectRole)" v-if="isUIAllowed('excelImport', { roles: projectRole })"
key="quick-import-excel" key="quick-import-excel"
@click="openQuickImportDialog('excel')" @click="openQuickImportDialog('excel')"
> >

32
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { stringifyRolesObj } from 'nocodb-sdk'
import type { BaseType, ProjectType, TableType } from 'nocodb-sdk' import type { BaseType, ProjectType, TableType } from 'nocodb-sdk'
import { LoadingOutlined } from '@ant-design/icons-vue' import { LoadingOutlined } from '@ant-design/icons-vue'
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
@ -43,6 +44,8 @@ const { activeTable } = storeToRefs(useTablesStore())
const { appInfo, navigateToProject } = useGlobal() const { appInfo, navigateToProject } = useGlobal()
const { orgRoles, isUIAllowed } = useRoles()
useTabs() useTabs()
const editMode = ref(false) const editMode = ref(false)
@ -53,8 +56,6 @@ const { t } = useI18n()
const input = ref<HTMLInputElement>() const input = ref<HTMLInputElement>()
const { isUIAllowed } = useUIPermission()
const projectRole = inject(ProjectRoleInj) const projectRole = inject(ProjectRoleInj)
const { activeProjectId } = storeToRefs(useProjects()) const { activeProjectId } = storeToRefs(useProjects())
@ -438,7 +439,11 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</span> </span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div> <div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div>
<NcDropdown v-if="isUIAllowed('tableCreate', false, projectRole)" v-model:visible="isOptionsOpen" :trigger="['click']"> <NcDropdown
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-model:visible="isOptionsOpen"
:trigger="['click']"
>
<NcButton <NcButton
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }" :class="{ '!text-black !opacity-100': isOptionsOpen }"
@ -469,7 +474,10 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<GeneralIcon icon="copy" class="group-hover:text-black" /> <GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }} {{ $t('activity.account.projInfo') }}
</NcMenuItem> </NcMenuItem>
<NcMenuItem v-if="isUIAllowed('duplicateProject', true, projectRole)" @click="duplicateProject(project)"> <NcMenuItem
v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
@click="duplicateProject(project)"
>
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }}
</NcMenuItem> </NcMenuItem>
@ -493,7 +501,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</template> </template>
<!-- Team & Settings --> <!-- Team & Settings -->
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('settings')" v-if="isUIAllowed('settingsPage')"
key="teamAndSettings" key="teamAndSettings"
v-e="['c:navdraw:project-settings']" v-e="['c:navdraw:project-settings']"
class="nc-sidebar-project-project-settings" class="nc-sidebar-project-project-settings"
@ -510,7 +518,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</template> </template>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectDelete', false, projectRole)" v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500 !hover:bg-red-50"
@click="isProjectDeleteDialogVisible = true" @click="isProjectDeleteDialogVisible = true"
> >
@ -522,7 +530,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</NcDropdown> </NcDropdown>
<NcButton <NcButton
v-if="isUIAllowed('tableCreate', false, projectRole)" v-if="isUIAllowed('tableCreate', { roles: projectRole })"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
size="xxsmall" size="xxsmall"
type="text" type="text"
@ -604,7 +612,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</a-tooltip> </a-tooltip>
</div> </div>
<div <div
v-if="isUIAllowed('tableCreate', false, projectRole)" v-if="isUIAllowed('tableCreate', { roles: projectRole })"
class="flex flex-row items-center gap-x-0.25 w-12.25" class="flex flex-row items-center gap-x-0.25 w-12.25"
> >
<NcDropdown <NcDropdown
@ -642,7 +650,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</NcDropdown> </NcDropdown>
<NcButton <NcButton
v-if="isUIAllowed('tableCreate', false, projectRole)" v-if="isUIAllowed('tableCreate', { roles: projectRole })"
type="text" type="text"
size="xxsmall" size="xxsmall"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
@ -676,7 +684,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<template v-else-if="contextMenuTarget.type === 'base'"></template> <template v-else-if="contextMenuTarget.type === 'base'"></template>
<template v-else-if="contextMenuTarget.type === 'table'"> <template v-else-if="contextMenuTarget.type === 'table'">
<NcMenuItem v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value, true)"> <NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<div class="nc-project-option-item"> <div class="nc-project-option-item">
<GeneralIcon icon="edit" class="text-gray-700" /> <GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('general.rename') }}
@ -684,7 +692,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('table-duplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)" v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
@click="duplicateTable(contextMenuTarget.value)" @click="duplicateTable(contextMenuTarget.value)"
> >
<div class="nc-project-option-item"> <div class="nc-project-option-item">
@ -693,7 +701,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem v-if="isUIAllowed('table-delete')" @click="isTableDeleteDialogVisible = true"> <NcMenuItem v-if="isUIAllowed('tableDelete')" @click="isTableDeleteDialogVisible = true">
<div class="nc-project-option-item text-red-600"> <div class="nc-project-option-item text-red-600">
<GeneralIcon icon="delete" /> <GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('general.delete') }}

18
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -5,7 +5,7 @@ import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useNuxtApp } from '#app' import { useNuxtApp } from '#app'
import { ProjectRoleInj, TreeViewInj, useTabs, useUIPermission } from '#imports' import { ProjectRoleInj, TreeViewInj, useRoles, useTabs } from '#imports'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -22,7 +22,7 @@ const baseIndex = toRef(props, 'baseIndex')
const route = useRoute() const route = useRoute()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const tabStore = useTabs() const tabStore = useTabs()
const { updateTab } = tabStore const { updateTab } = tabStore
@ -71,7 +71,7 @@ const { isSharedBase } = useProject()
// const isMultiBase = computed(() => project.bases && project.bases.length > 1) // const isMultiBase = computed(() => project.bases && project.bases.length > 1)
const canUserEditEmote = computed(() => { const canUserEditEmote = computed(() => {
return isUIAllowed('tableIconCustomisation', false, projectRole?.value) return isUIAllowed('tableIconEdit', { roles: projectRole?.value })
}) })
</script> </script>
@ -125,7 +125,7 @@ const canUserEditEmote = computed(() => {
v-if="table.type === 'table'" v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm" class="flex w-5 !text-gray-500 text-sm"
:class="{ :class="{
'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop', false, projectRole), 'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'!text-black': openedTableId === table.id, '!text-black': openedTableId === table.id,
}" }"
/> />
@ -133,7 +133,7 @@ const canUserEditEmote = computed(() => {
v-else v-else
class="flex w-5 !text-gray-500 text-sm" class="flex w-5 !text-gray-500 text-sm"
:class="{ :class="{
'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop', false, projectRole), 'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'!text-black': openedTableId === table.id, '!text-black': openedTableId === table.id,
}" }"
/> />
@ -157,7 +157,7 @@ const canUserEditEmote = computed(() => {
<NcDropdown <NcDropdown
v-if=" v-if="
!isSharedBase && (isUIAllowed('table-rename', false, projectRole) || isUIAllowed('table-delete', false, projectRole)) !isSharedBase && (isUIAllowed('tableRename', { roles: projectRole }) || isUIAllowed('tableDelete', { roles: projectRole }))
" "
:trigger="['click']" :trigger="['click']"
@click.stop @click.stop
@ -173,7 +173,7 @@ const canUserEditEmote = computed(() => {
<template #overlay> <template #overlay>
<NcMenu> <NcMenu>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('table-rename', false, projectRole)" v-if="isUIAllowed('tableRename', { roles: projectRole })"
:data-testid="`sidebar-table-rename-${table.title}`" :data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, project.bases[baseIndex].id)" @click="openRenameTableDialog(table, project.bases[baseIndex].id)"
> >
@ -183,7 +183,7 @@ const canUserEditEmote = computed(() => {
<NcMenuItem <NcMenuItem
v-if=" v-if="
isUIAllowed('table-duplicate') && isUIAllowed('tableDuplicate') &&
project.bases?.[baseIndex] && project.bases?.[baseIndex] &&
(project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local) (project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local)
" "
@ -195,7 +195,7 @@ const canUserEditEmote = computed(() => {
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('table-delete', false, projectRole)" v-if="isUIAllowed('tableDelete', { roles: projectRole })"
:data-testid="`sidebar-table-delete-${table.title}`" :data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true" @click="isTableDeleteDialogVisible = true"

6
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -20,14 +20,14 @@ import {
useNuxtApp, useNuxtApp,
useProject, useProject,
useProjects, useProjects,
useRoles,
useTablesStore, useTablesStore,
useTabs, useTabs,
useUIPermission,
} from '#imports' } from '#imports'
import { useRouter } from '#app' import { useRouter } from '#app'
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { addTab } = useTabs() const { addTab } = useTabs()
@ -145,7 +145,7 @@ const duplicateTable = async (table: TableType) => {
const isCreateTableAllowed = computed( const isCreateTableAllowed = computed(
() => () =>
isUIAllowed('table-create') && isUIAllowed('tableCreate') &&
route.value.name !== 'index' && route.value.name !== 'index' &&
route.value.name !== 'index-index' && route.value.name !== 'index-index' &&
route.value.name !== 'index-index-create' && route.value.name !== 'index-index-create' &&

4
packages/nc-gui/components/general/AddBaseButton.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { iconMap } from '#imports' import { iconMap } from '#imports'
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { t } = useI18n() const { t } = useI18n()
@ -10,7 +10,7 @@ const toggleDialog = inject(ToggleDialogInj, () => {})
<template> <template>
<div <div
v-if="isUIAllowed('settings')" v-if="isUIAllowed('settingsPage')"
class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)" class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)"
@click="toggleDialog(true, undefined, undefined, projectId)" @click="toggleDialog(true, undefined, undefined, projectId)"
> >

9
packages/nc-gui/components/general/PreviewAs.vue

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ProjectRole, iconMap, onUnmounted, ref, useEventListener, useGlobal, useI18n, useNuxtApp, watch } from '#imports' import { ProjectRoles } from 'nocodb-sdk'
import { iconMap, onUnmounted, ref, useEventListener, useGlobal, useI18n, useNuxtApp, watch } from '#imports'
import MdiAccountStar from '~icons/mdi/account-star' import MdiAccountStar from '~icons/mdi/account-star'
import MdiAccountHardHat from '~icons/mdi/account-hard-hat' import MdiAccountHardHat from '~icons/mdi/account-hard-hat'
import PhPencilCircleThin from '~icons/ph/pencil-circle-thin' import PhPencilCircleThin from '~icons/ph/pencil-circle-thin'
@ -14,9 +15,9 @@ const { t } = useI18n()
const { previewAs } = useGlobal() const { previewAs } = useGlobal()
const roleList = [ const roleList = [
{ value: ProjectRole.Editor, label: t('objects.roleType.editor') }, { value: ProjectRoles.EDITOR, label: t('objects.roleType.editor') },
{ value: ProjectRole.Commenter, label: t('objects.roleType.commenter') }, { value: ProjectRoles.COMMENTER, label: t('objects.roleType.commenter') },
{ value: ProjectRole.Viewer, label: t('objects.roleType.viewer') }, { value: ProjectRoles.VIEWER, label: t('objects.roleType.viewer') },
] ]
const roleIcon = { const roleIcon = {

4
packages/nc-gui/components/general/ShareBaseButton.vue

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { isDrawerOrModalExist, isMac, useNuxtApp, useRoute, useUIPermission } from '#imports' import { isDrawerOrModalExist, isMac, useNuxtApp, useRoles, useRoute } from '#imports'
const route = useRoute() const route = useRoute()
const showUserModal = ref(false) const showUserModal = ref(false)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()

6
packages/nc-gui/components/general/ShareProject.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { isDrawerOrModalExist, isMac, useNuxtApp } from '#imports' import { isDrawerOrModalExist, isMac, useNuxtApp, useRoles } from '#imports'
interface Props { interface Props {
disabled?: boolean disabled?: boolean
@ -15,7 +15,7 @@ const { project } = storeToRefs(useProject())
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
useEventListener(document, 'keydown', async (e: KeyboardEvent) => { useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
@ -36,7 +36,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template> <template>
<div <div
v-if="isUIAllowed('shareProject') && visibility !== 'hidden' && (activeTable || project)" v-if="isUIAllowed('projectShare') && visibility !== 'hidden' && (activeTable || project)"
class="flex flex-col justify-center h-full" class="flex flex-col justify-center h-full"
data-testid="share-project-button" data-testid="share-project-button"
:data-sharetype="visibility" :data-sharetype="visibility"

7
packages/nc-gui/components/project/AllTables.vue

@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import { stringifyRolesObj } from 'nocodb-sdk'
import type { BaseType, TableType } from 'nocodb-sdk' import type { BaseType, TableType } from 'nocodb-sdk'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -7,9 +6,7 @@ const { activeTables } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore() const { openTable } = useTablesStore()
const { openedProject } = storeToRefs(useProjects()) const { openedProject } = storeToRefs(useProjects())
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { allRoles } = useRoles()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -74,7 +71,7 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
<template> <template>
<div class="nc-all-tables-view"> <div class="nc-all-tables-view">
<div v-if="isUIAllowed('tableCreate', false, stringifyRolesObj(allRoles))" class="flex flex-row gap-x-6 pb-3 pt-6"> <div v-if="isUIAllowed('tableCreate')" class="flex flex-row gap-x-6 pb-3 pt-6">
<div class="nc-project-view-all-table-btn" data-testid="proj-view-btn__add-new-table" @click="openTableCreateDialog()"> <div class="nc-project-view-all-table-btn" data-testid="proj-view-btn__add-new-table" @click="openTableCreateDialog()">
<GeneralIcon icon="addOutlineBox" /> <GeneralIcon icon="addOutlineBox" />
<div class="label">{{ $t('general.new') }} {{ $t('objects.table') }}</div> <div class="label">{{ $t('general.new') }} {{ $t('objects.table') }}</div>

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

@ -130,19 +130,19 @@ const copyUrl = async () => {
{{ role }} {{ role }}
</div> </div>
</div> --> </div> -->
<NcBadge v-if="role === ProjectRole.Owner" color="purple"> <NcBadge v-if="role === ProjectRoles.OWNER" color="purple">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if="role === ProjectRole.Creator" color="blue"> <NcBadge v-if="role === ProjectRoles.CREATOR" color="blue">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if="role === ProjectRole.Editor" color="green"> <NcBadge v-if="role === ProjectRoles.EDITOR" color="green">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if="role === ProjectRole.Commenter" color="orange"> <NcBadge v-if="role === ProjectRoles.COMMENTER" color="orange">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if="role === ProjectRole.Viewer" color="yellow"> <NcBadge v-if="role === ProjectRoles.VIEWER" color="yellow">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
</a-select-option> </a-select-option>

6
packages/nc-gui/components/project/View.vue

@ -14,7 +14,7 @@ const route = router.currentRoute
return openedProject.value?.bases?.[0] return openedProject.value?.bases?.[0]
}) */ }) */
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
@ -97,7 +97,7 @@ watch(
<!-- <a-tab-pane v-if="defaultBase" key="erd" tab="Project ERD" force-render class="pt-4 pb-12"> <!-- <a-tab-pane v-if="defaultBase" key="erd" tab="Project ERD" force-render class="pt-4 pb-12">
<ErdView :base-id="defaultBase!.id" class="!h-full" /> <ErdView :base-id="defaultBase!.id" class="!h-full" />
</a-tab-pane> --> </a-tab-pane> -->
<a-tab-pane v-if="isUIAllowed('shareProject')" key="collaborator"> <a-tab-pane v-if="isUIAllowed('newUser')" key="collaborator">
<template #tab> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__access-settings"> <div class="tab-title" data-testid="proj-view-tab__access-settings">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" /> <GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
@ -106,7 +106,7 @@ watch(
</template> </template>
<ProjectAccessSettings /> <ProjectAccessSettings />
</a-tab-pane> </a-tab-pane>
<a-tab-pane v-if="isUIAllowed('createBase')" key="data-source"> <a-tab-pane v-if="isUIAllowed('baseCreate')" key="data-source">
<template #tab> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__data-sources"> <div class="tab-title" data-testid="proj-view-tab__data-sources">
<GeneralIcon icon="database" /> <GeneralIcon icon="database" />

6
packages/nc-gui/components/smartsheet/Cell.vue

@ -252,7 +252,11 @@ onUnmounted(() => {
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" /> <LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" /> <LazyCellText v-else v-model="vModel" />
<div <div
v-if="(isLocked || (isPublic && readOnly && !isForm) || isSystemColumn(column)) && !isAttachment(column) && !isTextArea(column)" v-if="
(isLocked || (isPublic && readOnly && !isForm) || isSystemColumn(column)) &&
!isAttachment(column) &&
!isTextArea(column)
"
class="nc-locked-overlay" class="nc-locked-overlay"
/> />
</template> </template>

2
packages/nc-gui/components/smartsheet/Details.vue

@ -2,7 +2,7 @@
const { openedViewsTab } = storeToRefs(useViewsStore()) const { openedViewsTab } = storeToRefs(useViewsStore())
const { onViewsTabChange } = useViewsStore() const { onViewsTabChange } = useViewsStore()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const openedSubTab = computed({ const openedSubTab = computed({
get() { get() {

8
packages/nc-gui/components/smartsheet/Form.vue

@ -22,7 +22,7 @@ import {
useGlobal, useGlobal,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useUIPermission, useRoles,
useViewColumns, useViewColumns,
useViewData, useViewData,
watch, watch,
@ -42,13 +42,13 @@ const formRef = ref()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const formState = reactive({}) const formState = reactive({})
const secondsRemain = ref(0) const secondsRemain = ref(0)
const isEditable = isUIAllowed('editFormView' as Permission) const isEditable = isUIAllowed('viewFieldEdit' as Permission)
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
@ -592,7 +592,7 @@ watch(view, (nextView) => {
@click="activeRow = element.title" @click="activeRow = element.title"
> >
<div <div
v-if="isUIAllowed('editFormView') && !isRequired(element, element.required)" v-if="isUIAllowed('viewFieldEdit') && !isRequired(element, element.required)"
class="absolute flex top-2 right-2" class="absolute flex top-2 right-2"
> >
<component <component

4
packages/nc-gui/components/smartsheet/Gallery.vue

@ -84,8 +84,8 @@ const isRowEmpty = (record: any, col: any) => {
return Array.isArray(val) && val.length === 0 return Array.isArray(val) && val.length === 0
} }
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('xcDatatableEditable')) const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
// TODO: extract this code (which is duplicated in grid and gallery) into a separate component // TODO: extract this code (which is duplicated in grid and gallery) into a separate component
const _contextMenu = ref(false) const _contextMenu = ref(false)
const contextMenu = computed({ const contextMenu = computed({

4
packages/nc-gui/components/smartsheet/Kanban.vue

@ -89,7 +89,7 @@ const {
const { isViewDataLoading } = storeToRefs(useViewsStore()) const { isViewDataLoading } = storeToRefs(useViewsStore())
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { appInfo } = useGlobal() const { appInfo } = useGlobal()
@ -103,7 +103,7 @@ provide(IsGridInj, ref(false))
provide(IsKanbanInj, ref(true)) provide(IsKanbanInj, ref(true))
const hasEditPermission = computed(() => isUIAllowed('xcDatatableEditable')) const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))

4
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { IsPublicInj, inject, ref, useSharedView, useSmartsheetStoreOrThrow, useUIPermission, useViewsStore } from '#imports' import { IsPublicInj, inject, ref, useRoles, useSharedView, useSmartsheetStoreOrThrow, useViewsStore } from '#imports'
const { isGrid, isGallery, isKanban, isMap } = useSmartsheetStoreOrThrow() const { isGrid, isGallery, isKanban, isMap } = useSmartsheetStoreOrThrow()
@ -9,7 +9,7 @@ const { isViewsLoading } = storeToRefs(useViewsStore())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { allowCSVDownload } = useSharedView() const { allowCSVDownload } = useSharedView()

17
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -1,7 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import type { AuditType } from 'nocodb-sdk' import type { AuditType } from 'nocodb-sdk'
import { enumColor, iconMap, ref, timeAgo, useCopy, useExpandedFormStoreOrThrow, useGlobal, useI18n, watch } from '#imports' import {
enumColor,
iconMap,
ref,
timeAgo,
useCopy,
useExpandedFormStoreOrThrow,
useGlobal,
useI18n,
useRoles,
watch,
} from '#imports'
const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment, updateComment } = const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment, updateComment } =
useExpandedFormStoreOrThrow() useExpandedFormStoreOrThrow()
@ -18,9 +29,9 @@ const { t } = useI18n()
const { user } = useGlobal() const { user } = useGlobal()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('commentEditable')) const hasEditPermission = computed(() => isUIAllowed('commentEdit'))
// currently, edit option is disable on purpose // currently, edit option is disable on purpose
// since the current update wouldn't keep track of the previous values // since the current update wouldn't keep track of the previous values

12
packages/nc-gui/components/smartsheet/expanded-form/Header.vue

@ -7,9 +7,9 @@ import {
isMac, isMac,
useCopy, useCopy,
useExpandedFormStoreOrThrow, useExpandedFormStoreOrThrow,
useRoles,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const props = defineProps<{ view?: ViewType }>() const props = defineProps<{ view?: ViewType }>()
@ -24,7 +24,7 @@ const { commentsDrawer, displayValue, primaryKey, save: _save, loadRow, deleteRo
const { isNew, syncLTARRefs, state } = useSmartsheetRowStoreOrThrow() const { isNew, syncLTARRefs, state } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
@ -62,7 +62,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
if (cmdOrCtrl) { if (cmdOrCtrl) {
switch (e.key) { switch (e.key) {
case 'Enter': { case 'Enter': {
if (isUIAllowed('tableRowUpdate')) { if (isUIAllowed('dataEdit')) {
await save() await save()
} }
} }
@ -129,7 +129,7 @@ const onConfirmDeleteRowClick = async () => {
class="nc-expand-form-save-btn !w-[60px]" class="nc-expand-form-save-btn !w-[60px]"
type="primary" type="primary"
size="small" size="small"
:disabled="!isUIAllowed('tableRowUpdate')" :disabled="!isUIAllowed('dataEdit')"
@click="save" @click="save"
> >
{{ $t('general.save') }} {{ $t('general.save') }}
@ -148,7 +148,7 @@ const onConfirmDeleteRowClick = async () => {
{{ $t('general.reload') }} {{ $t('general.reload') }}
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="isUIAllowed('xcDatatableEditable') && !isNew" @click="!isNew && emit('duplicateRow')"> <a-menu-item v-if="isUIAllowed('dataEdit') && !isNew" @click="!isNew && emit('duplicateRow')">
<div v-e="['c:row-expand:duplicate']" class="py-2 flex gap-2 a"> <div v-e="['c:row-expand:duplicate']" class="py-2 flex gap-2 a">
<component <component
:is="iconMap.copy" :is="iconMap.copy"
@ -157,7 +157,7 @@ const onConfirmDeleteRowClick = async () => {
{{ $t('activity.duplicateRow') }} {{ $t('activity.duplicateRow') }}
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="isUIAllowed('xcDatatableEditable') && !isNew" @click="!isNew && onDeleteRowClick()"> <a-menu-item v-if="isUIAllowed('dataEdit') && !isNew" @click="!isNew && onDeleteRowClick()">
<div v-e="['c:row-expand:delete']" class="py-2 flex gap-2 items-center"> <div v-e="['c:row-expand:delete']" class="py-2 flex gap-2 items-center">
<component <component
:is="iconMap.delete" :is="iconMap.delete"

3
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -21,6 +21,7 @@ import {
useActiveKeyupListener, useActiveKeyupListener,
useProvideExpandedFormStore, useProvideExpandedFormStore,
useProvideSmartsheetStore, useProvideSmartsheetStore,
useRoles,
useRouter, useRouter,
useVModel, useVModel,
watch, watch,
@ -57,7 +58,7 @@ const meta = toRef(props, 'meta')
const router = useRouter() const router = useRouter()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
// override cell click hook to avoid unexpected behavior at form fields // override cell click hook to avoid unexpected behavior at form fields
provide(CellClickHookInj, undefined) provide(CellClickHookInj, undefined)

18
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes, WorkspaceUserRoles, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
@ -33,7 +33,6 @@ import {
useRoles, useRoles,
useRoute, useRoute,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
useUndoRedo, useUndoRedo,
useViewsStore, useViewsStore,
watch, watch,
@ -172,10 +171,9 @@ const cellRefs = ref<{ el: HTMLElement }[]>([])
const gridRect = useElementBounding(gridWrapper) const gridRect = useElementBounding(gridWrapper)
// #Permissions // #Permissions
const { hasRole } = useRoles() const { isUIAllowed } = useRoles()
const { isUIAllowed } = useUIPermission() const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const hasEditPermission = computed(() => isUIAllowed('xcDatatableEditable')) const isAddingColumnAllowed = computed(() => !readOnly.value && !isLocked.value && isUIAllowed('fieldAdd') && !isSqlView.value)
const isAddingColumnAllowed = computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value)
// #Variables // #Variables
const addColumnDropdown = ref(false) const addColumnDropdown = ref(false)
@ -1350,13 +1348,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
<span class="flex-1" /> <span class="flex-1" />
<div <div
v-if=" v-if="isUIAllowed('expandedForm')"
!readOnly ||
hasRole('commenter', true) ||
hasRole('viewer', true) ||
hasRole(WorkspaceUserRoles.COMMENTER, true) ||
hasRole(WorkspaceUserRoles.VIEWER, true)
"
class="nc-expand" class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`" :data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }" :class="{ 'nc-comment': row.rowMeta?.commentCount }"

12
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnReqType, ColumnType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { ColumnInj, IsExpandedFormOpenInj, IsFormInj, IsKanbanInj, inject, provide, ref, toRef, useUIPermission } from '#imports' import { ColumnInj, IsExpandedFormOpenInj, IsFormInj, IsKanbanInj, inject, provide, ref, toRef, useRoles } from '#imports'
interface Props { interface Props {
column: ColumnType column: ColumnType
@ -23,7 +23,7 @@ const isKanban = inject(IsKanbanInj, ref(false))
const column = toRef(props, 'column') const column = toRef(props, 'column')
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
provide(ColumnInj, column) provide(ColumnInj, column)
@ -42,13 +42,13 @@ const closeAddColumnDropdown = () => {
} }
const openHeaderMenu = () => { const openHeaderMenu = () => {
if (!isForm.value && !isExpandedForm.value && isUIAllowed('edit-column')) { if (!isForm.value && !isExpandedForm.value && isUIAllowed('fieldEdit')) {
editColumnDropdown.value = true editColumnDropdown.value = true
} }
} }
const openDropDown = () => { const openDropDown = () => {
if (isForm.value || isExpandedForm.value || !isUIAllowed('edit-column')) return if (isForm.value || isExpandedForm.value || !isUIAllowed('fieldEdit')) return
isDropDownOpen.value = !isDropDownOpen.value isDropDownOpen.value = !isDropDownOpen.value
} }
</script> </script>
@ -65,7 +65,7 @@ const openDropDown = () => {
<div <div
v-if="column" v-if="column"
class="name pl-1 !truncate" class="name pl-1 !truncate"
:class="{ 'cursor-pointer pt-0.25': !isForm && isUIAllowed('edit-column') && !hideMenu }" :class="{ 'cursor-pointer pt-0.25': !isForm && isUIAllowed('fieldEdit') && !hideMenu }"
style="white-space: pre-line" style="white-space: pre-line"
:title="column.title" :title="column.title"
> >
@ -77,7 +77,7 @@ const openDropDown = () => {
<template v-if="!hideMenu"> <template v-if="!hideMenu">
<div class="flex-1" /> <div class="flex-1" />
<LazySmartsheetHeaderMenu <LazySmartsheetHeaderMenu
v-if="!isForm && !isExpandedForm && isUIAllowed('edit-column')" v-if="!isForm && !isExpandedForm && isUIAllowed('fieldEdit')"
v-model:is-open="isDropDownOpen" v-model:is-open="isDropDownOpen"
@add-column="addField" @add-column="addField"
@edit="openHeaderMenu" @edit="openHeaderMenu"

6
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -19,7 +19,7 @@ import {
toRef, toRef,
useI18n, useI18n,
useMetas, useMetas,
useUIPermission, useRoles,
} from '#imports' } from '#imports'
const props = defineProps<{ column: ColumnType; hideMenu?: boolean; required?: boolean | number; hideIcon?: boolean }>() const props = defineProps<{ column: ColumnType; hideMenu?: boolean; required?: boolean | number; hideIcon?: boolean }>()
@ -38,7 +38,7 @@ provide(ColumnInj, column)
const { metas } = useMetas() const { metas } = useMetas()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
@ -149,7 +149,7 @@ const closeAddColumnDropdown = () => {
<div class="flex-1" /> <div class="flex-1" />
<LazySmartsheetHeaderMenu <LazySmartsheetHeaderMenu
v-if="!isForm && isUIAllowed('edit-column') && !isExpandedForm" v-if="!isForm && isUIAllowed('fieldEdit') && !isExpandedForm"
v-model:is-open="isDropDownOpen" v-model:is-open="isDropDownOpen"
:virtual="true" :virtual="true"
@add-column="addField" @add-column="addField"

8
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -2,7 +2,7 @@
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk' import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity' import type { WritableComputedRef } from '@vue/reactivity'
import { IsLockedInj, inject, message, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports' import { IsLockedInj, inject, message, onKeyStroke, useDebounceFn, useNuxtApp, useRoles, useVModel } from '#imports'
interface Props { interface Props {
view: ViewType view: ViewType
@ -31,7 +31,7 @@ const vModel = useVModel(props, 'view', emits) as WritableComputedRef<ViewType &
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
@ -59,7 +59,7 @@ const onClick = useDebounceFn(() => {
/** Enable editing view name on dbl click */ /** Enable editing view name on dbl click */
function onDblClick() { function onDblClick() {
if (!isUIAllowed('virtualViewsCreateOrEdit')) return if (!isUIAllowed('viewCreateOrEdit')) return
if (!isEditing.value) { if (!isEditing.value) {
isEditing.value = true isEditing.value = true
@ -222,7 +222,7 @@ watch(rightSidebarState, () => {
<div class="flex-1" /> <div class="flex-1" />
<template v-if="!isEditing && !isLocked && isUIAllowed('virtualViewsCreateOrEdit')"> <template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')">
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg"> <NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
<div <div
class="invisible !group-hover:visible" class="invisible !group-hover:visible"

6
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -10,9 +10,9 @@ import {
useCommandPalette, useCommandPalette,
useDialog, useDialog,
useNuxtApp, useNuxtApp,
useRoles,
useRoute, useRoute,
useRouter, useRouter,
useUIPermission,
useViewsStore, useViewsStore,
watch, watch,
} from '#imports' } from '#imports'
@ -39,7 +39,7 @@ const setLastOpenedViewId = (viewId?: string) => {
} }
} }
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const router = useRouter() const router = useRouter()
@ -203,7 +203,7 @@ function onOpenModal({
</div> </div>
<LazySmartsheetSidebarMenuTop v-else :views="views" @open-modal="onOpenModal" @deleted="loadViews" /> <LazySmartsheetSidebarMenuTop v-else :views="views" @open-modal="onOpenModal" @deleted="loadViews" />
</div> </div>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="flex flex-col"> <div v-if="isUIAllowed('viewCreateOrEdit')" class="flex flex-col">
<div class="!mb-3 w-full border-b-1 border-gray-200" /> <div class="!mb-3 w-full border-b-1 border-gray-200" />
<div v-if="!activeTable" class="flex flex-col pt-2 pb-5 px-6"> <div v-if="!activeTable" class="flex flex-col pt-2 pb-5 px-6">

2
packages/nc-gui/components/smartsheet/sidebar/toolbar/Developer.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const isPublicView = inject(IsPublicInj, ref(false)) const isPublicView = inject(IsPublicInj, ref(false))

6
packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue

@ -9,10 +9,10 @@ import {
ref, ref,
useKanbanViewStoreOrThrow, useKanbanViewStoreOrThrow,
useMenuCloseOnEsc, useMenuCloseOnEsc,
useUIPermission, useRoles,
} from '#imports' } from '#imports'
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { groupingFieldColumn } = useKanbanViewStoreOrThrow() const { groupingFieldColumn } = useKanbanViewStoreOrThrow()
@ -33,7 +33,7 @@ provide(IsKanbanInj, ref(true))
<template> <template>
<a-dropdown <a-dropdown
v-if="!IsPublic && isUIAllowed('edit-column')" v-if="!IsPublic && isUIAllowed('fieldEdit')"
v-model:visible="open" v-model:visible="open"
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-dropdown-kanban-add-edit-stack-menu" overlay-class-name="nc-dropdown-kanban-add-edit-stack-menu"

6
packages/nc-gui/components/smartsheet/toolbar/MoreActions.vue

@ -15,9 +15,9 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoles,
useSharedView, useSharedView,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const { t } = useI18n() const { t } = useI18n()
@ -48,7 +48,7 @@ const showWebhookDrawer = ref(false)
const quickImportDialog = ref(false) const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const exportFile = async (exportType: ExportTypes) => { const exportFile = async (exportType: ExportTypes) => {
let offset = 0 let offset = 0
@ -146,7 +146,7 @@ const exportFile = async (exportType: ExportTypes) => {
</div> </div>
<div <div
v-if="isUIAllowed('sharedViewList') && !isView && !isPublicView" v-if="isUIAllowed('viewShare') && !isView && !isPublicView"
v-e="['a:actions:shared-view-list']" v-e="['a:actions:shared-view-list']"
class="nc-menu-item" class="nc-menu-item"
@click="sharedViewListDlg = true" @click="sharedViewListDlg = true"

6
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -16,8 +16,8 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoles,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
watch, watch,
} from '#imports' } from '#imports'
import type { SharedView } from '#imports' import type { SharedView } from '#imports'
@ -32,7 +32,7 @@ const { $e } = useNuxtApp()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { isSharedBase } = storeToRefs(useProject()) const { isSharedBase } = storeToRefs(useProject())
@ -249,7 +249,7 @@ watch(shared, () => {
<template> <template>
<div> <div>
<a-button <a-button
v-if="isUIAllowed('share-view') && !isSharedBase" v-if="isUIAllowed('viewShare') && !isSharedBase"
v-e="['c:view:share']" v-e="['c:view:share']"
outlined outlined
class="nc-btn-share-view nc-toolbar-btn" class="nc-btn-share-view nc-toolbar-btn"

6
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -17,8 +17,8 @@ import {
useMenuCloseOnEsc, useMenuCloseOnEsc,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoles,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const { t } = useI18n() const { t } = useI18n()
@ -56,7 +56,7 @@ const quickImportDialogs: Record<(typeof quickImportDialogTypes)[number], Ref<bo
{}, {},
) as Record<QuickImportDialogType, Ref<boolean>> ) as Record<QuickImportDialogType, Ref<boolean>>
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
useProject() useProject()
@ -161,7 +161,7 @@ useMenuCloseOnEsc(open)
</a-sub-menu> </a-sub-menu>
<a-sub-menu <a-sub-menu
v-if="isUIAllowed('view-type')" v-if="isUIAllowed('viewCreateOrEdit')"
key="lock-type" key="lock-type"
class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0" class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0"
> >

4
packages/nc-gui/components/tabs/Auth.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import { computed, useI18n, useUIPermission } from '#imports' import { computed, useI18n, useRoles } from '#imports'
interface Tab { interface Tab {
title: string title: string
@ -10,7 +10,7 @@ interface Tab {
const { t } = useI18n() const { t } = useI18n()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const tabsInfo: Tab[] = [ const tabsInfo: Tab[] = [
{ {

6
packages/nc-gui/components/tabs/Smartsheet.vue

@ -21,8 +21,8 @@ import {
useMetas, useMetas,
useProvideKanbanViewStore, useProvideKanbanViewStore,
useProvideSmartsheetStore, useProvideSmartsheetStore,
useRoles,
useSqlEditor, useSqlEditor,
useUIPermission,
} from '#imports' } from '#imports'
import type { TabItem } from '#imports' import type { TabItem } from '#imports'
@ -30,7 +30,7 @@ const props = defineProps<{
activeTab: TabItem activeTab: TabItem
}>() }>()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { metas, getMeta } = useMetas() const { metas, getMeta } = useMetas()
@ -76,7 +76,7 @@ provide(IsFormInj, isForm)
provide(TabMetaInj, activeTab) provide(TabMetaInj, activeTab)
provide( provide(
ReadonlyInj, ReadonlyInj,
computed(() => !isUIAllowed('xcDatatableEditable')), computed(() => !isUIAllowed('dataEdit')),
) )
const grid = ref() const grid = ref()

15
packages/nc-gui/components/tabs/auth/UserManagement.vue

@ -1,12 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { OrgUserRoles } from 'nocodb-sdk' import { OrgUserRoles, RoleColors } from 'nocodb-sdk'
import type { ProjectUserReqType, RequestParams } from 'nocodb-sdk' import type { ProjectUserReqType, RequestParams } from 'nocodb-sdk'
import { import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
iconMap, iconMap,
message, message,
onBeforeMount, onBeforeMount,
projectRoleTagColors,
ref, ref,
storeToRefs, storeToRefs,
useApi, useApi,
@ -15,7 +14,7 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
useUIPermission, useRoles,
watchDebounced, watchDebounced,
} from '#imports' } from '#imports'
import type { User } from '#imports' import type { User } from '#imports'
@ -30,7 +29,7 @@ const { project } = storeToRefs(useProject())
const { copy } = useCopy() const { copy } = useCopy()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
@ -278,15 +277,11 @@ const isSuperAdmin = (user: { main_roles?: string }) => {
<div <div
v-if="isSuperAdmin(user)" v-if="isSuperAdmin(user)"
class="rounded-full px-3 py-1 nc-user-role" class="rounded-full px-3 py-1 nc-user-role"
:style="{ backgroundColor: projectRoleTagColors[OrgUserRoles.SUPER_ADMIN] }" :style="{ backgroundColor: RoleColors[OrgUserRoles.SUPER_ADMIN] }"
> >
Super Admin Super Admin
</div> </div>
<div <div v-if="user.roles" class="rounded-full px-3 py-1 nc-user-role" :style="{ backgroundColor: RoleColors[user.roles] }">
v-if="user.roles"
class="rounded-full px-3 py-1 nc-user-role"
:style="{ backgroundColor: projectRoleTagColors[user.roles] }"
>
{{ $t(`objects.roleType.${user.roles}`) }} {{ $t(`objects.roleType.${user.roles}`) }}
</div> </div>
</div> </div>

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

@ -1,17 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Input } from 'ant-design-vue' import type { Input } from 'ant-design-vue'
import { ProjectRoles, RoleColors } from 'nocodb-sdk'
import type { ProjectUserReqType } from 'nocodb-sdk' import type { ProjectUserReqType } from 'nocodb-sdk'
import { import {
Form, Form,
ProjectRole,
computed, computed,
emailValidator, emailValidator,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
iconMap, iconMap,
message, message,
onMounted, onMounted,
projectRoleTagColors,
projectRoles,
ref, ref,
storeToRefs, storeToRefs,
useActiveKeyupListener, useActiveKeyupListener,
@ -44,7 +42,7 @@ const { copy } = useCopy()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
const usersData = ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invitationToken: undefined }) const usersData = ref<Users>({ emails: undefined, role: ProjectRoles.VIEWER, invitationToken: undefined })
const formRef = ref() const formRef = ref()
@ -68,7 +66,7 @@ onMounted(() => {
const close = () => { const close = () => {
emit('closed') emit('closed')
usersData.value = { role: ProjectRole.Viewer } usersData.value = { role: ProjectRoles.VIEWER }
} }
const saveUser = async () => { const saveUser = async () => {
@ -81,7 +79,7 @@ const saveUser = async () => {
try { try {
if (selectedUser?.id) { if (selectedUser?.id) {
await $api.auth.projectUserUpdate(project.value.id, selectedUser.id, { await $api.auth.projectUserUpdate(project.value.id, selectedUser.id, {
roles: usersData.value.role as ProjectRole, roles: usersData.value.role as ProjectRoles,
email: selectedUser.email, email: selectedUser.email,
project_id: project.value.id, project_id: project.value.id,
projectName: project.value.title, projectName: project.value.title,
@ -127,7 +125,7 @@ const copyUrl = async () => {
const clickInviteMore = () => { const clickInviteMore = () => {
$e('c:user:invite-more') $e('c:user:invite-more')
usersData.value.invitationToken = undefined usersData.value.invitationToken = undefined
usersData.value.role = ProjectRole.Viewer usersData.value.role = ProjectRoles.VIEWER
usersData.value.emails = undefined usersData.value.emails = undefined
} }
@ -244,12 +242,9 @@ watch(
class="nc-user-roles !rounded-md" class="nc-user-roles !rounded-md"
dropdown-class-name="nc-dropdown-user-role" dropdown-class-name="nc-dropdown-user-role"
> >
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option"> <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"> <div class="flex flex-row h-full justify-start items-center">
<div <div class="px-3 py-1 flex rounded-full text-xs" :style="{ backgroundColor: RoleColors[role] }">
class="px-3 py-1 flex rounded-full text-xs"
:style="{ backgroundColor: projectRoleTagColors[role] }"
>
{{ role }} {{ role }}
</div> </div>
</div> </div>

6
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -16,9 +16,9 @@ import {
inject, inject,
ref, ref,
useProvideLTARStore, useProvideLTARStore,
useRoles,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -39,7 +39,7 @@ const isLocked = inject(IsLockedInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false)) const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
@ -103,7 +103,7 @@ const belongsToColumn = computed(
</div> </div>
<div <div
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm) && !isUnderLookup" v-if="!readOnly && !isLocked && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
class="flex justify-end gap-1 min-h-[30px] items-center" class="flex justify-end gap-1 min-h-[30px] items-center"
> >
<GeneralIcon <GeneralIcon

6
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -14,9 +14,9 @@ import {
inject, inject,
ref, ref,
useProvideLTARStore, useProvideLTARStore,
useRoles,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -39,7 +39,7 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -129,7 +129,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
/> />
<GeneralIcon <GeneralIcon
v-if="(!readOnly && isUIAllowed('xcDatatableEditable')) || isForm" v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
icon="plus" icon="plus"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus" class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click.stop="listItemsDlg = true" @click.stop="listItemsDlg = true"

4
packages/nc-gui/components/virtual-cell/Links.vue

@ -27,7 +27,7 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { state, isNew } = useSmartsheetRowStoreOrThrow() const { state, isNew } = useSmartsheetRowStoreOrThrow()
@ -108,7 +108,7 @@ const localCellValue = computed<any[]>(() => {
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end hidden group-hover:flex items-center"> <div v-if="!isLocked && !isUnderLookup" class="flex justify-end hidden group-hover:flex items-center">
<MdiPlus <MdiPlus
v-if="(!readOnly && isUIAllowed('xcDatatableEditable')) || isForm" v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus" class="select-none !text-md text-gray-700 nc-action-icon nc-plus"
@click.stop="listItemsDlg = true" @click.stop="listItemsDlg = true"
/> />

6
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -16,9 +16,9 @@ import {
inject, inject,
ref, ref,
useProvideLTARStore, useProvideLTARStore,
useRoles,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -41,7 +41,7 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -131,7 +131,7 @@ const m2mColumn = computed(
/> />
<GeneralIcon <GeneralIcon
v-if="!readOnly && isUIAllowed('xcDatatableEditable')" v-if="!readOnly && isUIAllowed('dataEdit')"
icon="plus" icon="plus"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click.stop="listItemsDlg = true" @click.stop="listItemsDlg = true"

4
packages/nc-gui/components/virtual-cell/components/ItemChip.vue

@ -28,7 +28,7 @@ const emit = defineEmits(['unlink'])
const { relatedTableMeta } = useLTARStoreOrThrow()! const { relatedTableMeta } = useLTARStoreOrThrow()!
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
@ -97,7 +97,7 @@ export default {
<div <div
v-show="active || isForm" v-show="active || isForm"
v-if="showUnlinkButton && !readOnly && !isLocked && isUIAllowed('xcDatatableEditable')" v-if="showUnlinkButton && !readOnly && !isLocked && isUIAllowed('dataEdit')"
class="flex items-center" class="flex items-center"
> >
<component <component

4
packages/nc-gui/components/workspace/CreateProjectBtn.vue

@ -10,7 +10,7 @@ const props = defineProps<{
centered?: boolean centered?: boolean
}>() }>()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { orgRoles, workspaceRoles } = useRoles() const { orgRoles, workspaceRoles } = useRoles()
@ -28,7 +28,7 @@ const centered = computed(() => props.centered ?? true)
<template> <template>
<NcButton <NcButton
v-if="isUIAllowed('projectCreate', false, workspaceRoles ?? orgRoles) && !isSharedBase" v-if="isUIAllowed('projectCreate', { roles: workspaceRoles ?? orgRoles }) && !isSharedBase"
type="text" type="text"
:size="size" :size="size"
:centered="centered" :centered="centered"

22
packages/nc-gui/components/workspace/ProjectList.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Empty } from 'ant-design-vue' import { Empty } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk' import type { ProjectType } from 'nocodb-sdk'
import { ProjectStatus, WorkspaceUserRoles } from 'nocodb-sdk' import { ProjectRoles, ProjectStatus, WorkspaceUserRoles } from 'nocodb-sdk'
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import { NcProjectType, isEeUI, navigateTo, storeToRefs, timeAgo, useGlobal, useWorkspace } from '#imports' import { NcProjectType, isEeUI, navigateTo, storeToRefs, timeAgo, useGlobal, useWorkspace } from '#imports'
import { useNuxtApp } from '#app' import { useNuxtApp } from '#app'
@ -20,7 +20,7 @@ const { navigateToProject } = useGlobal()
const { $e, $jobs } = useNuxtApp() const { $e, $jobs } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
@ -40,11 +40,11 @@ const roleAlias = {
[WorkspaceUserRoles.CREATOR]: 'Workspace Creator', [WorkspaceUserRoles.CREATOR]: 'Workspace Creator',
[WorkspaceUserRoles.EDITOR]: 'Workspace Editor', [WorkspaceUserRoles.EDITOR]: 'Workspace Editor',
[WorkspaceUserRoles.COMMENTER]: 'Workspace Commenter', [WorkspaceUserRoles.COMMENTER]: 'Workspace Commenter',
[ProjectRole.Creator]: 'Project Creator', [ProjectRoles.CREATOR]: 'Project Creator',
[ProjectRole.Editor]: 'Project Editor', [ProjectRoles.EDITOR]: 'Project Editor',
[ProjectRole.Viewer]: 'Project Viewer', [ProjectRoles.VIEWER]: 'Project Viewer',
[ProjectRole.Commenter]: 'Project Commenter', [ProjectRoles.COMMENTER]: 'Project Commenter',
[ProjectRole.Owner]: 'Project Owner', [ProjectRoles.OWNER]: 'Project Owner',
} }
const deleteProject = (project: ProjectType) => { const deleteProject = (project: ProjectType) => {
@ -326,7 +326,7 @@ const setIcon = async (icon: string, project: ProjectType) => {
<template v-if="column.dataIndex === 'id'"> <template v-if="column.dataIndex === 'id'">
<a-dropdown <a-dropdown
v-if="isUIAllowed('projectActionMenu', true, [record.workspace_role, record.project_role].join())" v-if="isUIAllowed('projectActionMenu', { roles: [record.workspace_role, record.project_role].join() })"
:trigger="['click']" :trigger="['click']"
> >
<div @click.stop> <div @click.stop>
@ -346,7 +346,7 @@ const setIcon = async (icon: string, project: ProjectType) => {
<a-menu-item <a-menu-item
v-if=" v-if="
record.type === NcProjectType.DB && record.type === NcProjectType.DB &&
isUIAllowed('duplicateProject', true, [record.workspace_role, record.project_role].join()) isUIAllowed('projectDuplicate', { roles: [record.workspace_role, record.project_role].join() })
" "
@click="duplicateProject(record)" @click="duplicateProject(record)"
> >
@ -357,7 +357,7 @@ const setIcon = async (icon: string, project: ProjectType) => {
</a-menu-item> </a-menu-item>
<!-- <!--
<a-menu-item <a-menu-item
v-if="false && isUIAllowed('moveProject', true, [record.workspace_role, record.project_role].join())" v-if="false && isUIAllowed('projectMove', { roles: [record.workspace_role, record.project_role].join() })"
@click="moveProject(record)" @click="moveProject(record)"
> >
<div class="nc-menu-item-wrapper"> <div class="nc-menu-item-wrapper">
@ -367,7 +367,7 @@ const setIcon = async (icon: string, project: ProjectType) => {
</a-menu-item> </a-menu-item>
--> -->
<a-menu-item <a-menu-item
v-if="isUIAllowed('projectDelete', true, [record.workspace_role, record.project_role].join())" v-if="isUIAllowed('projectDelete', { roles: [record.workspace_role, record.project_role].join() })"
@click="deleteProject(record)" @click="deleteProject(record)"
> >
<div class="nc-menu-item-wrapper text-red-500"> <div class="nc-menu-item-wrapper text-red-500">

2
packages/nc-gui/composables/useExpandedFormStore.ts

@ -57,7 +57,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
// getters // getters
const displayValue = computed(() => { const displayValue = computed(() => {

5
packages/nc-gui/composables/useGlobal/types.ts

@ -1,7 +1,8 @@
import type { ComputedRef, Ref, ToRefs } from 'vue' import type { ComputedRef, Ref, ToRefs } from 'vue'
import type { WritableComputedRef } from '@vue/reactivity' import type { WritableComputedRef } from '@vue/reactivity'
import type { JwtPayload } from 'jwt-decode' import type { JwtPayload } from 'jwt-decode'
import type { Language, NcProjectType, ProjectRole, User, useCounter } from '#imports' import type { ProjectRoles } from 'nocodb-sdk'
import type { Language, NcProjectType, User, useCounter } from '#imports'
export interface AppInfo { export interface AppInfo {
ncSiteUrl: string ncSiteUrl: string
@ -36,7 +37,7 @@ export interface StoredState {
lang: keyof typeof Language lang: keyof typeof Language
darkMode: boolean darkMode: boolean
filterAutoSave: boolean filterAutoSave: boolean
previewAs: ProjectRole | null previewAs: ProjectRoles | null
includeM2M: boolean includeM2M: boolean
showNull: boolean showNull: boolean
currentVersion: string | null currentVersion: string | null

17
packages/nc-gui/composables/useGridViewColumn.ts

@ -1,23 +1,12 @@
import type { ColumnType, GridColumnReqType, GridColumnType, ViewType } from 'nocodb-sdk' import type { ColumnType, GridColumnReqType, GridColumnType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { import { IsPublicInj, computed, inject, ref, useMetas, useNuxtApp, useRoles, useStyleTag, useUndoRedo, watch } from '#imports'
IsPublicInj,
computed,
inject,
ref,
useMetas,
useNuxtApp,
useStyleTag,
useUIPermission,
useUndoRedo,
watch,
} from '#imports'
const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState( const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>, statePublic = false) => { (view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>, statePublic = false) => {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('') const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -99,7 +88,7 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
} }
// sync with server if allowed // sync with server if allowed
if (!isPublic.value && isUIAllowed('gridColUpdate') && gridViewCols.value[id]?.id) { if (!isPublic.value && isUIAllowed('viewFieldEdit') && gridViewCols.value[id]?.id) {
await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, { await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
...props, ...props,
}) })

6
packages/nc-gui/composables/useKanbanViewStore.ts

@ -21,9 +21,9 @@ import {
useInjectionState, useInjectionState,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoles,
useSharedView, useSharedView,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
useUndoRedo, useUndoRedo,
} from '#imports' } from '#imports'
@ -51,7 +51,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
const { sharedView, fetchSharedViewData, fetchSharedViewGroupedData } = useSharedView() const { sharedView, fetchSharedViewData, fetchSharedViewGroupedData } = useSharedView()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const isPublic = ref(shared) || inject(IsPublicInj, ref(false)) const isPublic = ref(shared) || inject(IsPublicInj, ref(false))
@ -313,7 +313,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
} }
async function updateKanbanMeta(updateObj: Partial<KanbanType>) { async function updateKanbanMeta(updateObj: Partial<KanbanType>) {
if (!viewMeta?.value?.id || !isUIAllowed('xcDatatableEditable')) return if (!viewMeta?.value?.id || !isUIAllowed('dataEdit')) return
await $api.dbView.kanbanUpdate(viewMeta.value.id, updateObj) await $api.dbView.kanbanUpdate(viewMeta.value.id, updateObj)
} }

4
packages/nc-gui/composables/useMapViewDataStore.ts

@ -34,7 +34,7 @@ const [useProvideMapViewStore, useMapViewStore] = useInjectionState(
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const isPublic = ref(shared) || inject(IsPublicInj, ref(false)) const isPublic = ref(shared) || inject(IsPublicInj, ref(false))
@ -86,7 +86,7 @@ const [useProvideMapViewStore, useMapViewStore] = useInjectionState(
} }
async function updateMapMeta(updateObj: Partial<MapType>) { async function updateMapMeta(updateObj: Partial<MapType>) {
if (!viewMeta?.value?.id || !isUIAllowed('xcDatatableEditable')) return if (!viewMeta?.value?.id || !isUIAllowed('dataEdit')) return
await $api.dbView.mapUpdate(viewMeta.value.id, updateObj) await $api.dbView.mapUpdate(viewMeta.value.id, updateObj)
} }

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

@ -1,6 +1,22 @@
import { isString } from '@vue/shared'
import type { Roles, RolesObj, WorkspaceUserRoles } from 'nocodb-sdk'
import { extractRolesObj } from 'nocodb-sdk' import { extractRolesObj } from 'nocodb-sdk'
import { computed, createSharedComposable, useApi, useGlobal } from '#imports' import { computed, createSharedComposable, rolePermissions, useApi, useGlobal } from '#imports'
import type { ProjectRole, Role, Roles } from '#imports' import type { Permission } from '#imports'
const hasPermission = (role: Exclude<Roles, WorkspaceUserRoles>, hasRole: boolean, permission: Permission | string) => {
const rolePermission = rolePermissions[role]
if (!hasRole || !rolePermission) return false
if (isString(rolePermission) && rolePermission === '*') return true
if ('include' in rolePermission && rolePermission.include) {
return !!rolePermission.include[permission as keyof typeof rolePermission.include]
}
return rolePermission[permission as keyof typeof rolePermission]
}
/** /**
* Provides the roles a user currently has * Provides the roles a user currently has
@ -8,15 +24,14 @@ import type { ProjectRole, Role, Roles } from '#imports'
* * `userRoles` - the roles a user has outside of projects * * `userRoles` - the roles a user has outside of projects
* * `projectRoles` - the roles a user has in the current project (if one was loaded) * * `projectRoles` - the roles a user has in the current project (if one was loaded)
* * `allRoles` - all roles a user has (userRoles + projectRoles) * * `allRoles` - all roles a user has (userRoles + projectRoles)
* * `hasRole` - a function to check if a user has a specific role
* * `loadRoles` - a function to load reload user roles for scope * * `loadRoles` - a function to load reload user roles for scope
*/ */
export const useRoles = createSharedComposable(() => { export const useRoles = createSharedComposable(() => {
const { user, previewAs } = useGlobal() const { user } = useGlobal()
const { api } = useApi() const { api } = useApi()
const allRoles = computed<Roles | null>(() => { const allRoles = computed<RolesObj | null>(() => {
let orgRoles = user.value?.roles ?? {} let orgRoles = user.value?.roles ?? {}
orgRoles = extractRolesObj(orgRoles) orgRoles = extractRolesObj(orgRoles)
@ -31,7 +46,7 @@ export const useRoles = createSharedComposable(() => {
} }
}) })
const orgRoles = computed<Roles | null>(() => { const orgRoles = computed<RolesObj | null>(() => {
let orgRoles = user.value?.roles ?? {} let orgRoles = user.value?.roles ?? {}
orgRoles = extractRolesObj(orgRoles) orgRoles = extractRolesObj(orgRoles)
@ -39,7 +54,7 @@ export const useRoles = createSharedComposable(() => {
return orgRoles return orgRoles
}) })
const projectRoles = computed<Roles | null>(() => { const projectRoles = computed<RolesObj | null>(() => {
let projectRoles = user.value?.project_roles ?? {} let projectRoles = user.value?.project_roles ?? {}
if (Object.keys(projectRoles).length === 0) { if (Object.keys(projectRoles).length === 0) {
@ -51,7 +66,7 @@ export const useRoles = createSharedComposable(() => {
return projectRoles return projectRoles
}) })
const workspaceRoles = computed<Roles | null>(() => { const workspaceRoles = computed<RolesObj | null>(() => {
return null return null
}) })
@ -75,7 +90,7 @@ export const useRoles = createSharedComposable(() => {
...user.value, ...user.value,
roles: res.roles, roles: res.roles,
project_roles: res.project_roles, project_roles: res.project_roles,
} } as typeof User
} else if (options?.isSharedErd) { } else if (options?.isSharedErd) {
const res = await api.auth.me( const res = await api.auth.me(
{ {
@ -92,7 +107,7 @@ export const useRoles = createSharedComposable(() => {
...user.value, ...user.value,
roles: res.roles, roles: res.roles,
project_roles: res.project_roles, project_roles: res.project_roles,
} } as typeof User
} else if (projectId) { } else if (projectId) {
const res = await api.auth.me({ project_id: projectId }) const res = await api.auth.me({ project_id: projectId })
@ -100,17 +115,28 @@ export const useRoles = createSharedComposable(() => {
...user.value, ...user.value,
roles: res.roles, roles: res.roles,
project_roles: res.project_roles, project_roles: res.project_roles,
} } as typeof User
} }
} }
function hasRole(role: Role | ProjectRole | string, includePreviewRoles = false) { const isUIAllowed = (
if (previewAs.value && includePreviewRoles) { permission: Permission | string,
return previewAs.value === role args: { roles?: string | Record<string, boolean> | string[] | null } = {},
) => {
const { roles } = args
let checkRoles: Record<string, boolean> = {}
if (!roles) {
if (allRoles.value) checkRoles = allRoles.value
} else {
checkRoles = extractRolesObj(roles)
} }
return allRoles.value[role] return Object.entries(checkRoles).some(([role, hasRole]) =>
hasPermission(role as Exclude<Roles, WorkspaceUserRoles>, hasRole, permission),
)
} }
return { allRoles, orgRoles, workspaceRoles, projectRoles, loadRoles, hasRole } return { allRoles, orgRoles, workspaceRoles, projectRoles, loadRoles, isUIAllowed }
}) })

61
packages/nc-gui/composables/useUIPermission/index.ts

@ -1,61 +0,0 @@
import { isString } from '@vue/shared'
import { createSharedComposable, rolePermissions, useGlobal, useRoles } from '#imports'
import type { Permission, ProjectRole, Role } from '#imports'
const hasPermission = (role: Role | ProjectRole, hasRole: boolean, permission: Permission | string) => {
const rolePermission = rolePermissions[role]
if (!hasRole || !rolePermission) return false
if (isString(rolePermission) && rolePermission === '*') return true
if ('include' in rolePermission && rolePermission.include) {
return !!rolePermission.include[permission as keyof typeof rolePermission.include]
}
if ('exclude' in rolePermission && rolePermission.exclude) {
return !rolePermission.exclude[permission as keyof typeof rolePermission.exclude]
}
return rolePermission[permission as keyof typeof rolePermission]
}
export const useUIPermission = createSharedComposable(() => {
const { previewAs } = useGlobal()
const { allRoles } = useRoles()
const isUIAllowed = (
permission: Permission | string,
skipPreviewAs = false,
userRoles: string | Record<string, boolean> | string[] | null = null,
combineWithStateRoles = false,
) => {
if (previewAs.value && !skipPreviewAs) {
return hasPermission(previewAs.value, true, permission)
}
let roles: Record<string, boolean> = {}
if (!userRoles) {
roles = allRoles.value
} else if (Array.isArray(userRoles) || typeof userRoles === 'string') {
roles = (Array.isArray(userRoles) ? userRoles : userRoles.split(','))
// filter out any empty-string/null/undefined values
.filter(Boolean)
.reduce<Record<string, boolean>>((acc, role) => {
acc[role] = true
return acc
}, {})
} else if (typeof userRoles === 'object') {
roles = userRoles
}
if (userRoles && combineWithStateRoles) {
roles = { ...roles, ...allRoles.value }
}
return Object.entries(roles).some(([role, hasRole]) => hasPermission(role as Role | ProjectRole, hasRole, permission))
}
return { isUIAllowed }
})

0
packages/nc-gui/composables/useUIPermission/rolePermissions.ts

19
packages/nc-gui/composables/useViewColumns.ts

@ -1,18 +1,7 @@
import { ViewTypes, isSystemColumn } from 'nocodb-sdk' import { ViewTypes, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, MapType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { import { IsPublicInj, computed, inject, ref, storeToRefs, useNuxtApp, useProject, useRoles, useUndoRedo, watch } from '#imports'
IsPublicInj,
computed,
inject,
ref,
storeToRefs,
useNuxtApp,
useProject,
useUIPermission,
useUndoRedo,
watch,
} from '#imports'
import type { Field } from '#imports' import type { Field } from '#imports'
export function useViewColumns( export function useViewColumns(
@ -28,14 +17,14 @@ export function useViewColumns(
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { isSharedBase } = storeToRefs(useProject()) const { isSharedBase } = storeToRefs(useProject())
const { addUndo, defineViewScope } = useUndoRedo() const { addUndo, defineViewScope } = useUndoRedo()
const isLocalMode = computed( const isLocalMode = computed(
() => isPublic.value || !isUIAllowed('hideAllColumns') || !isUIAllowed('showAllColumns') || isSharedBase.value, () => isPublic.value || !isUIAllowed('viewFieldEdit') || !isUIAllowed('viewFieldEdit') || isSharedBase.value,
) )
const localChanges = ref<Field[]>([]) const localChanges = ref<Field[]>([])
@ -169,7 +158,7 @@ export function useViewColumns(
localChanges.value.push(field) localChanges.value.push(field)
} }
if (isUIAllowed('fieldsSync')) { if (isUIAllowed('viewFieldEdit')) {
if (field.id && view?.value?.id) { if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field) await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view.value?.id) { } else if (view.value?.id) {

6
packages/nc-gui/composables/useViewData.ts

@ -16,11 +16,11 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoles,
useRouter, useRouter,
useSharedView, useSharedView,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useState, useState,
useUIPermission,
} from '#imports' } from '#imports'
import type { Row } from '#imports' import type { Row } from '#imports'
@ -76,7 +76,7 @@ export function useViewData(
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const routeQuery = computed(() => route.value.query as Record<string, string>) const routeQuery = computed(() => route.value.query as Record<string, string>)
@ -136,7 +136,7 @@ export function useViewData(
/** load row comments count */ /** load row comments count */
async function loadAggCommentsCount() { async function loadAggCommentsCount() {
if (!isUIAllowed('commentsCount')) return if (!isUIAllowed('commentCount')) return
if (isPublic.value || isSharedBase.value) return if (isPublic.value || isSharedBase.value) return

4
packages/nc-gui/composables/useViewFilters.ts

@ -16,7 +16,7 @@ import {
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useProject, useProject,
useUIPermission, useRoles,
watch, watch,
} from '#imports' } from '#imports'
import type { Filter, UndoRedoAction } from '#imports' import type { Filter, UndoRedoAction } from '#imports'
@ -42,7 +42,7 @@ export function useViewFilters(
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { metas } = useMetas() const { metas } = useMetas()

2
packages/nc-gui/composables/useViewGroupBy.ts

@ -34,7 +34,7 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
const isGroupBy = computed(() => !!groupBy.value.length) const isGroupBy = computed(() => !!groupBy.value.length)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()

4
packages/nc-gui/composables/useViewSorts.ts

@ -12,9 +12,9 @@ import {
storeToRefs, storeToRefs,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoles,
useSharedView, useSharedView,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
export function useViewSorts(view: Ref<ViewType | undefined>, reloadData?: () => void) { export function useViewSorts(view: Ref<ViewType | undefined>, reloadData?: () => void) {
@ -24,7 +24,7 @@ export function useViewSorts(view: Ref<ViewType | undefined>, reloadData?: () =>
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const { isSharedBase } = storeToRefs(useProject()) const { isSharedBase } = storeToRefs(useProject())

2
packages/nc-gui/layouts/base.vue

@ -98,7 +98,7 @@ hooks.hook('page:finish', () => {
</a-menu-item> </a-menu-item>
<a-menu-divider class="!m-0" /> <a-menu-divider class="!m-0" />
<!-- <a-menu-item v-if="isUIAllowed('appStore')" key="0" class="!rounded-t"> <!-- <a-menu-item v-if="isUIAllowed('superAdminAppStore')" key="0" class="!rounded-t">
<nuxt-link <nuxt-link
v-e="['c:settings:appstore', { page: true }]" v-e="['c:settings:appstore', { page: true }]"
class="nc-project-menu-item group !no-underline" class="nc-project-menu-item group !no-underline"

2
packages/nc-gui/layouts/new.vue

@ -99,7 +99,7 @@ export default {
</a-menu-item> </a-menu-item>
<a-menu-divider class="!m-0" /> <a-menu-divider class="!m-0" />
<!-- <a-menu-item v-if="isUIAllowed('appStore')" key="0" class="!rounded-t"> <!-- <a-menu-item v-if="isUIAllowed('superAdminAppStore')" key="0" class="!rounded-t">
<nuxt-link <nuxt-link
v-e="['c:settings:appstore', { page: true }]" v-e="['c:settings:appstore', { page: true }]"
class="nc-project-menu-item group !no-underline" class="nc-project-menu-item group !no-underline"

148
packages/nc-gui/lib/acl.ts

@ -0,0 +1,148 @@
import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk'
const roleScopes = {
org: [OrgUserRoles.VIEWER, OrgUserRoles.CREATOR],
project: [ProjectRoles.VIEWER, ProjectRoles.COMMENTER, ProjectRoles.EDITOR, ProjectRoles.CREATOR, ProjectRoles.OWNER],
}
interface Perm {
include?: Record<string, boolean>
}
/**
* Each permission value means the following
* `*` - which is wildcard, means all permissions are allowed
* `include` - which is an object, means only the permissions listed in the object are allowed
* `undefined` or `{}` - which is the default value, means no permissions are allowed
* */
const rolePermissions = {
// org level role permissions
[OrgUserRoles.SUPER_ADMIN]: '*',
[OrgUserRoles.CREATOR]: {
include: {
projectCreate: true,
projectMove: true,
projectDelete: true,
projectDuplicate: true,
newUser: true,
},
},
[OrgUserRoles.VIEWER]: {
include: {
importRequest: true,
},
},
// Project role permissions
[ProjectRoles.OWNER]: {
include: {
projectDelete: true,
},
},
[ProjectRoles.CREATOR]: {
include: {
baseCreate: true,
fieldUpdate: true,
hookList: true,
tableCreate: true,
tableRename: true,
tableDelete: true,
tableDuplicate: true,
tableSort: true,
layoutRename: true,
layoutDelete: true,
airtableImport: true,
jsonImport: true,
excelImport: true,
settingsPage: true,
newUser: true,
webhook: true,
fieldEdit: true,
fieldAdd: true,
tableIconEdit: true,
viewCreateOrEdit: true,
viewShare: true,
projectShare: true,
},
},
[ProjectRoles.EDITOR]: {
include: {
dataInsert: true,
dataEdit: true,
sortSync: true,
filterSync: true,
filterChildrenRead: true,
viewFieldEdit: true,
csvImport: true,
apiDocs: true,
},
},
[ProjectRoles.COMMENTER]: {
include: {
commentEdit: true,
commentList: true,
commentCount: true,
},
},
[ProjectRoles.VIEWER]: {
include: {
projectSettings: true,
expandedForm: true,
},
},
[ProjectRoles.NO_ACCESS]: {
include: {},
},
} as Record<OrgUserRoles | ProjectRoles, Perm | '*'>
/*
We inherit include permissions from previous roles in the same scope (role order)
To determine role order, we use `roleScopes` object
So for example ProjectRoles.COMMENTER has `commentEdit` permission,
which means ProjectRoles.EDITOR, ProjectRoles.CREATOR, ProjectRoles.OWNER will also have `commentEdit` permission
where as ProjectRoles.VIEWER, ProjectRoles.NO_ACCESS will not have `commentEdit` permission.
This is why we are validating that there are no duplicate permissions within the same scope
even though it is not required for the code to work. It is to keep the code clean and easy to understand.
*/
// validate no duplicate permissions within same scope
Object.values(roleScopes).forEach((roles) => {
const scopePermissions: Record<string, boolean> = {}
const duplicates: string[] = []
roles.forEach((role) => {
const perms = (rolePermissions[role] as Perm).include || {}
Object.keys(perms).forEach((perm) => {
if (scopePermissions[perm]) {
duplicates.push(perm)
}
scopePermissions[perm] = true
})
})
if (duplicates.length) {
throw new Error(
`Duplicate permissions found in roles ${roles.join(', ')}. Please remove duplicate permissions: ${duplicates.join(', ')}`,
)
}
})
// inherit include permissions within scope (role order)
Object.values(roleScopes).forEach((roles) => {
let roleIndex = 0
for (const role of roles) {
if (roleIndex === 0) {
roleIndex++
continue
}
if (rolePermissions[role] === '*') continue
if ((rolePermissions[role] as Perm).include && (rolePermissions[roles[roleIndex - 1]] as Perm).include) {
Object.assign((rolePermissions[role] as Perm).include!, (rolePermissions[roles[roleIndex - 1]] as Perm).include)
}
roleIndex++
}
})
export { rolePermissions }

89
packages/nc-gui/lib/constants.ts

@ -1,5 +1,3 @@
import { ProjectRole, Role } from './enums'
export const NOCO = 'noco' export const NOCO = 'noco'
export const SYSTEM_COLUMNS = ['id', 'title', 'created_at', 'updated_at'] export const SYSTEM_COLUMNS = ['id', 'title', 'created_at', 'updated_at']
@ -18,90 +16,3 @@ export const GROUP_BY_VARS = {
__nc_false__: 'Unchecked', __nc_false__: 'Unchecked',
} as Record<string, string>, } as Record<string, string>,
} }
/**
* Each permission value means the following
* `*` - which is wildcard, means all permissions are allowed
* `include` - which is an object, means only the permissions listed in the object are allowed
* `exclude` - which is an object, means all permissions are allowed except the ones listed in the object
* `undefined` or `{}` - which is the default value, means no permissions are allowed
* */
const rolePermissions = {
// general role permissions
[Role.Super]: '*',
[Role.Admin]: {} as Record<string, boolean>,
[Role.Guest]: {} as Record<string, boolean>,
[Role.OrgLevelCreator]: {
include: {
projectCreate: true,
projectActions: true,
projectSettings: true,
},
},
// Project role permissions
[ProjectRole.Creator]: {
exclude: {
appStore: true,
superAdminUserManagement: true,
superAdminAppSettings: true,
appLicense: true,
moveProject: true,
projectDelete: true,
projectCreate: true,
},
},
[ProjectRole.Owner]: {
exclude: {
projectCreate: true,
appStore: true,
superAdminUserManagement: true,
superAdminAppSettings: true,
appLicense: true,
},
},
[ProjectRole.Editor]: {
include: {
smartSheet: true,
xcDatatableEditable: true,
column: true,
tableAttachment: true,
tableRowUpdate: true,
dataInsert: true,
rowComments: true,
gridViewOptions: true,
sortSync: true,
fieldsSync: true,
gridColUpdate: true,
filterSync: true,
filterChildrenRead: true,
csvImport: true,
apiDocs: true,
projectSettings: true,
newUser: false,
commentEditable: true,
commentList: true,
commentsCount: true,
},
},
[ProjectRole.Commenter]: {
include: {
smartSheet: true,
column: true,
rowComments: true,
projectSettings: true,
commentEditable: true,
commentList: true,
commentsCount: true,
},
},
[ProjectRole.Viewer]: {
include: {
smartSheet: true,
column: true,
projectSettings: true,
},
},
} as const
export { rolePermissions }

21
packages/nc-gui/lib/enums.ts

@ -1,24 +1,3 @@
export enum Role {
Super = 'super',
Admin = 'admin',
OrgLevelCreator = 'org-level-creator',
OrgLevelViewer = 'org-level-viewer',
WorkspaceLevelCreator = 'workspace-level-creator',
WorkspaceLevelViewer = 'workspace-level-viewer',
WorkspaceLevelOwner = 'workspace-level-owner',
Guest = 'guest',
}
export enum ProjectRole {
Owner = 'owner',
Creator = 'creator',
Editor = 'editor',
Commenter = 'commenter',
Viewer = 'viewer',
}
export enum ClientType { export enum ClientType {
MYSQL = 'mysql2', MYSQL = 'mysql2',
MSSQL = 'mssql', MSSQL = 'mssql',

1
packages/nc-gui/lib/index.ts

@ -1,3 +1,4 @@
export * from './constants' export * from './constants'
export * from './enums' export * from './enums'
export * from './types' export * from './types'
export * from './acl'

31
packages/nc-gui/lib/types.ts

@ -1,17 +1,8 @@
import type { import type { ColumnType, FilterType, MetaType, PaginatedType, ProjectType, Roles, RolesObj, ViewTypes } from 'nocodb-sdk'
ColumnType,
FilterType,
MetaType,
OrgUserRoles,
PaginatedType,
ProjectType,
ViewTypes,
WorkspaceUserRoles,
} from 'nocodb-sdk'
import type { I18n } from 'vue-i18n' import type { I18n } from 'vue-i18n'
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider' import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider'
import type { UploadFile } from 'ant-design-vue' import type { UploadFile } from 'ant-design-vue'
import type { ImportSource, ImportType, ProjectRole, Role, TabType } from './enums' import type { ImportSource, ImportType, TabType } from './enums'
import type { rolePermissions } from './constants' import type { rolePermissions } from './constants'
interface User { interface User {
@ -19,9 +10,9 @@ interface User {
email: string email: string
firstname: string | null firstname: string | null
lastname: string | null lastname: string | null
roles: Roles | string roles: RolesObj
project_roles: Roles | string project_roles: RolesObj
workspace_roles: Roles | string workspace_roles: RolesObj
invite_token?: string invite_token?: string
project_id?: string project_id?: string
display_name?: string | null display_name?: string | null
@ -47,8 +38,6 @@ interface Field {
isViewEssentialField?: boolean isViewEssentialField?: boolean
} }
type Roles<T extends Role | ProjectRole = Role | ProjectRole> = Record<T | string, boolean>
type Filter = FilterType & { type Filter = FilterType & {
field?: string field?: string
status?: 'update' | 'delete' | 'create' status?: 'update' | 'delete' | 'create'
@ -171,15 +160,9 @@ interface GroupNestedIn {
column_uidt: string column_uidt: string
} }
type AllRoles =
| (typeof ProjectRole)[keyof typeof ProjectRole]
| (typeof Role)[keyof typeof Role]
| (typeof WorkspaceUserRoles)[keyof typeof WorkspaceUserRoles]
| (typeof OrgUserRoles)[keyof typeof OrgUserRoles]
interface Users { interface Users {
emails?: string emails?: string
role: AllRoles role: Roles
invitationToken?: string invitationToken?: string
} }
@ -191,7 +174,6 @@ export {
User, User,
ProjectMetaInfo, ProjectMetaInfo,
Field, Field,
Roles,
Filter, Filter,
NocoI18n, NocoI18n,
ThemeConfig, ThemeConfig,
@ -209,7 +191,6 @@ export {
ImportWorkerPayload, ImportWorkerPayload,
Group, Group,
GroupNestedIn, GroupNestedIn,
AllRoles,
Users, Users,
ViewPageType, ViewPageType,
NcButtonSize, NcButtonSize,

6
packages/nc-gui/pages/account/index.vue

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { iconMap, navigateTo, useUIPermission } from '#imports' import { iconMap, navigateTo, useRoles } from '#imports'
definePageMeta({ definePageMeta({
hideHeader: true, hideHeader: true,
}) })
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const $route = useRoute() const $route = useRoute()
@ -86,7 +86,7 @@ const logout = async () => {
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('appStore') && !isEeUI" v-if="isUIAllowed('superAdminAppStore') && !isEeUI"
key="apps" key="apps"
class="item" class="item"
:class="{ :class="{

4
packages/nc-gui/pages/account/index/users/[[nestedPage]].vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUIPermission } from '#imports' import { useRoles } from '#imports'
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
</script> </script>
<template> <template>

4
packages/nc-gui/store/projectsShortcuts.ts

@ -3,7 +3,7 @@ import { isDrawerOrModalExist, useEventListener } from '#imports'
export const useProjectsShortcuts = defineStore('projectsShortcutsStore', () => { export const useProjectsShortcuts = defineStore('projectsShortcutsStore', () => {
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useRoles()
const isMounted = ref(false) const isMounted = ref(false)
@ -38,7 +38,7 @@ export const useProjectsShortcuts = defineStore('projectsShortcutsStore', () =>
} }
// 'ALT + ,' // 'ALT + ,'
case 188: { case 188: {
if (isUIAllowed('settings') && !isDrawerOrModalExist()) { if (isUIAllowed('settingsPage') && !isDrawerOrModalExist()) {
$e('c:shortcut', { key: 'ALT + ,' }) $e('c:shortcut', { key: 'ALT + ,' })
const projectsStore = useProjects() const projectsStore = useProjects()

5
packages/nc-gui/store/share.ts

@ -1,4 +1,5 @@
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { ProjectRoles } from 'nocodb-sdk'
import type { Users } from '#imports' import type { Users } from '#imports'
export const useShare = defineStore('share', () => { export const useShare = defineStore('share', () => {
@ -28,7 +29,7 @@ export const useShare = defineStore('share', () => {
const showShareModal = ref(false) const showShareModal = ref(false)
const invitationUsersData = ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invitationToken: undefined }) const invitationUsersData = ref<Users>({ emails: undefined, role: ProjectRoles.VIEWER, invitationToken: undefined })
watch( watch(
[isProjectPublic], [isProjectPublic],
@ -41,7 +42,7 @@ export const useShare = defineStore('share', () => {
const resetData = () => { const resetData = () => {
formStatus.value = 'project-collaborate' formStatus.value = 'project-collaborate'
invitationValid.value = false invitationValid.value = false
invitationUsersData.value = { emails: undefined, role: ProjectRole.Viewer, invitationToken: undefined } invitationUsersData.value = { emails: undefined, role: ProjectRoles.VIEWER, invitationToken: undefined }
isInvitationLinkCopied.value = false isInvitationLinkCopied.value = false
} }

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

@ -1,3 +1,4 @@
import { OrgUserRoles } from 'nocodb-sdk'
import type { ProjectType } from 'nocodb-sdk' import type { ProjectType } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@ -66,17 +67,17 @@ export const useWorkspace = defineStore('workspaceStore', () => {
/** getters */ /** getters */
const isWorkspaceCreator = computed(() => { const isWorkspaceCreator = computed(() => {
// todo: type correction // todo: type correction
return orgRoles.value?.[Role.OrgLevelCreator] return orgRoles.value?.[OrgUserRoles.CREATOR]
}) })
const isWorkspaceOwner = computed(() => { const isWorkspaceOwner = computed(() => {
// todo: type correction // todo: type correction
return orgRoles.value?.[Role.OrgLevelCreator] return orgRoles.value?.[OrgUserRoles.CREATOR]
}) })
const isWorkspaceOwnerOrCreator = computed(() => { const isWorkspaceOwnerOrCreator = computed(() => {
// todo: type correction // todo: type correction
return orgRoles.value?.[Role.OrgLevelCreator] return orgRoles.value?.[OrgUserRoles.CREATOR]
}) })
/** actions */ /** actions */

1
packages/nc-gui/utils/index.ts

@ -16,7 +16,6 @@ export * from './validation'
export * from './viewUtils' export * from './viewUtils'
export * from './currencyUtils' export * from './currencyUtils'
export * from './dataUtils' export * from './dataUtils'
export * from './userUtils'
export * from './stringUtils' export * from './stringUtils'
export * from './memStorage' export * from './memStorage'
export * from './browserUtils' export * from './browserUtils'

15
packages/nc-gui/utils/userUtils.ts

@ -1,15 +0,0 @@
import { OrgUserRoles } from 'nocodb-sdk'
import { ProjectRole } from '../lib/enums'
export const projectRoleTagColors = {
[ProjectRole.Owner]: '#cfdffe',
[ProjectRole.Creator]: '#d0f1fd',
[ProjectRole.Editor]: '#c2f5e8',
[ProjectRole.Commenter]: '#ffdaf6',
[ProjectRole.Viewer]: '#ffdce5',
[OrgUserRoles.SUPER_ADMIN]: '#f5d7cb',
}
export const projectRoles = [ProjectRole.Creator, ProjectRole.Editor, ProjectRole.Commenter, ProjectRole.Viewer]
export const docsProjectRoles = [ProjectRole.Editor, ProjectRole.Viewer]

10
packages/nocodb-sdk/src/lib/globals.ts

@ -1,3 +1,5 @@
import { OrgUserRoles, ProjectRoles, WorkspaceUserRoles } from "./enums";
export enum ViewTypes { export enum ViewTypes {
FORM = 1, FORM = 1,
GALLERY = 2, GALLERY = 2,
@ -120,3 +122,11 @@ export enum TiptapMarksTypes {
code = 'code', code = 'code',
underline = 'underline', underline = 'underline',
} }
type Roles = OrgUserRoles | ProjectRoles | WorkspaceUserRoles;
type RolesObj = Partial<Record<Roles, boolean>>;
type RolesType = RolesObj | string[] | string;
export { Roles, RolesObj, RolesType };

12
packages/nocodb-sdk/src/lib/helperFunctions.ts

@ -1,5 +1,5 @@
import UITypes from './UITypes'; import UITypes from './UITypes';
import { OrgUserRoles, ProjectRoles, WorkspaceUserRoles } from './enums'; import { RolesObj, RolesType } from './globals';
// import {RelationTypes} from "./globals"; // import {RelationTypes} from "./globals";
@ -22,12 +22,7 @@ const isSystemColumn = (col): boolean =>
(col.pk && col.meta && col.meta.ag) || (col.pk && col.meta && col.meta.ag) ||
col.system); col.system);
type Roles = Record< const extractRolesObj = (roles: RolesType): RolesObj => {
OrgUserRoles | ProjectRoles | WorkspaceUserRoles | string,
boolean
>;
const extractRolesObj = (roles: Roles | string[] | string): Roles => {
if (!roles) return null; if (!roles) return null;
if (typeof roles === 'object' && !Array.isArray(roles)) return roles; if (typeof roles === 'object' && !Array.isArray(roles)) return roles;
@ -44,7 +39,8 @@ const extractRolesObj = (roles: Roles | string[] | string): Roles => {
}, {}); }, {});
}; };
const stringifyRolesObj = (roles: Roles): string => { const stringifyRolesObj = (roles?: RolesObj | null): string => {
if (!roles) return '';
const rolesArr = Object.keys(roles).filter((r) => roles[r]); const rolesArr = Object.keys(roles).filter((r) => roles[r]);
return rolesArr.join(','); return rolesArr.join(',');
}; };

Loading…
Cancel
Save