Browse Source

Merge branch 'develop' into feat/pnpm

pull/5903/head
Wing-Kam Wong 10 months ago
parent
commit
5b45565028
  1. 7
      packages/nc-gui/assets/nc-icons/plus-square.svg
  2. 31
      packages/nc-gui/assets/style.scss
  3. 12
      packages/nc-gui/components.d.ts
  4. 6
      packages/nc-gui/components/api-client/Params.vue
  5. 7
      packages/nc-gui/components/cell/DateTimePicker.vue
  6. 162
      packages/nc-gui/components/dashboard/Sidebar.vue
  7. 60
      packages/nc-gui/components/dashboard/Sidebar/Header.vue
  8. 86
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  9. 0
      packages/nc-gui/components/dashboard/Sidebar/TopSection/Header.vue
  10. 188
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  11. 1475
      packages/nc-gui/components/dashboard/TreeView.vue
  12. 0
      packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue
  13. 0
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  14. 30
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  15. 0
      packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue
  16. 0
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  17. 17
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  18. 86
      packages/nc-gui/components/dashboard/TreeView/index.vue
  19. 5
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  20. 1
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  21. 4
      packages/nc-gui/components/dlg/AirtableImport.vue
  22. 22
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  23. 54
      packages/nc-gui/components/dlg/TableDuplicate.vue
  24. 7
      packages/nc-gui/components/dlg/share-and-collaborate/View.vue
  25. 4
      packages/nc-gui/components/general/EmojiPicker.vue
  26. 4
      packages/nc-gui/components/general/MiniSidebar.vue
  27. 53
      packages/nc-gui/components/general/OpenLeftSidebarBtn.vue
  28. 8
      packages/nc-gui/components/general/ReleaseInfo.vue
  29. 51
      packages/nc-gui/components/general/UserIcon.vue
  30. 11
      packages/nc-gui/components/general/ViewIcon.vue
  31. 37
      packages/nc-gui/components/general/WorkspaceIcon.vue
  32. 2
      packages/nc-gui/components/general/language/index.vue
  33. 24
      packages/nc-gui/components/nc/Badge.vue
  34. 17
      packages/nc-gui/components/nc/Button.vue
  35. 9
      packages/nc-gui/components/nc/Divider.vue
  36. 24
      packages/nc-gui/components/nc/Dropdown.vue
  37. 19
      packages/nc-gui/components/nc/Menu.vue
  38. 17
      packages/nc-gui/components/nc/MenuItem.vue
  39. 4
      packages/nc-gui/components/nc/Modal.vue
  40. 18
      packages/nc-gui/components/nc/Tooltip.vue
  41. 22
      packages/nc-gui/components/project/AccessSettings.vue
  42. 2
      packages/nc-gui/components/project/AllTables.vue
  43. 10
      packages/nc-gui/components/project/InviteProjectCollabSection.vue
  44. 3
      packages/nc-gui/components/project/View.vue
  45. 5
      packages/nc-gui/components/smartsheet/Gallery.vue
  46. 43
      packages/nc-gui/components/smartsheet/Pagination.vue
  47. 14
      packages/nc-gui/components/smartsheet/Toolbar.vue
  48. 11
      packages/nc-gui/components/smartsheet/Topbar.vue
  49. 13
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  50. 28
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  51. 8
      packages/nc-gui/components/smartsheet/grid/Table.vue
  52. 8
      packages/nc-gui/components/smartsheet/header/Cell.vue
  53. 2
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  54. 8
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  55. 77
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  56. 2
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  57. 4
      packages/nc-gui/components/smartsheet/toolbar/Export.vue
  58. 2
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  59. 8
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  60. 53
      packages/nc-gui/components/smartsheet/toolbar/OpenViewSidebarBtn.vue
  61. 40
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  62. 9
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  63. 32
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  64. 14
      packages/nc-gui/components/smartsheet/topbar/SelectMode.vue
  65. 2
      packages/nc-gui/components/tabs/Smartsheet.vue
  66. 211
      packages/nc-gui/components/tabs/SmartsheetResizable.vue
  67. 8
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  68. 69
      packages/nc-gui/components/workspace/CreateProjectBtn.vue
  69. 21
      packages/nc-gui/components/workspace/CreateProjectDlg.vue
  70. 66
      packages/nc-gui/components/workspace/Delete.vue
  71. 21
      packages/nc-gui/components/workspace/EmptyPlaceholder.vue
  72. 6
      packages/nc-gui/components/workspace/InviteSection.vue
  73. 372
      packages/nc-gui/components/workspace/Menu.vue
  74. 164
      packages/nc-gui/components/workspace/Settings.vue
  75. 30
      packages/nc-gui/components/workspace/View.vue
  76. 46
      packages/nc-gui/composables/useApi/interceptors.ts
  77. 6
      packages/nc-gui/composables/useCommandPalette/index.ts
  78. 3
      packages/nc-gui/composables/useData.ts
  79. 49
      packages/nc-gui/composables/useGlobal/actions.ts
  80. 5
      packages/nc-gui/composables/useGlobal/state.ts
  81. 13
      packages/nc-gui/composables/useGlobal/types.ts
  82. 7
      packages/nc-gui/composables/useLTARStore.ts
  83. 85
      packages/nc-gui/composables/useRoles/index.ts
  84. 2
      packages/nc-gui/composables/useSharedView.ts
  85. 2
      packages/nc-gui/composables/useTableNew.ts
  86. 22
      packages/nc-gui/composables/useUndoRedo.ts
  87. 10
      packages/nc-gui/layouts/base.vue
  88. 208
      packages/nc-gui/layouts/dashboard.vue
  89. 10
      packages/nc-gui/layouts/empty.vue
  90. 2
      packages/nc-gui/layouts/new.vue
  91. 32
      packages/nc-gui/layouts/shared-view.vue
  92. 7
      packages/nc-gui/lib/types.ts
  93. 5
      packages/nc-gui/middleware/auth.global.ts
  94. 2
      packages/nc-gui/package.json
  95. 6
      packages/nc-gui/pages/account/index.vue
  96. 4
      packages/nc-gui/pages/forgot-password.vue
  97. 29
      packages/nc-gui/pages/index.vue
  98. 2
      packages/nc-gui/pages/index/[typeOrId].vue
  99. 52
      packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index.vue
  100. 6
      packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index/[viewId]/[[viewTitle]].vue
  101. Some files were not shown because too many files have changed in this diff Show More

7
packages/nc-gui/assets/nc-icons/plus-square.svg

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="plus-square">
<path id="Vector" d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M8 5.33334V10.6667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M5.33337 8H10.6667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 685 B

31
packages/nc-gui/assets/style.scss

@ -3,8 +3,8 @@
@import '@vue-flow/core/dist/theme-default.css';
:root {
--sidebar-top-height: 9.75rem;
--topbar-height: 3.1rem;
--sidebar-bottom-height: 8.5rem;
--new-header-height: 3.5rem;
--tw-text-opacity: 1;
--navbar-bg: #FAFAFA;
@ -90,6 +90,29 @@ main {
}
}
.nc-scrollbar-sm-dark {
overflow-y: scroll;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 2px;
height: 2px;
}
&::-webkit-scrollbar-track-piece {
width: 0px;
}
&::-webkit-scrollbar {
@apply bg-transparent;
}
&::-webkit-scrollbar-thumb {
width: 4px;
@apply bg-gray-300 ;
}
&::-webkit-scrollbar-thumb:hover {
@apply bg-gray-400;
}
}
.nc-scrollbar-x-md {
overflow-x: scroll;
@ -505,7 +528,7 @@ a {
padding: 0 !important;
}
.ant-popover-inner-content {
@apply !px-1.5 !py-1 text-xs text-white bg-black;
@apply !px-1.5 !py-1 text-xs;
}
.ant-tooltip-inner {
@apply !px-1.5 !py-1 text-xs text-white bg-black;
@ -515,4 +538,8 @@ a {
.ant-skeleton-input {
@apply !h-full;
}
.nc-toolbar-dropdown {
@apply !rounded-2xl;
}

12
packages/nc-gui/components.d.ts vendored

@ -50,6 +50,7 @@ declare module '@vue/runtime-core' {
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate']
@ -80,10 +81,12 @@ declare module '@vue/runtime-core' {
ClarityColorPickerSolid: typeof import('~icons/clarity/color-picker-solid')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
IcRoundStarBorder: typeof import('~icons/ic/round-star-border')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default']
@ -93,6 +96,8 @@ declare module '@vue/runtime-core' {
MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default']
MaterialSymbolsDeleteOutlineRounded: typeof import('~icons/material-symbols/delete-outline-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsGroupOutlineRounded: typeof import('~icons/material-symbols/group-outline-rounded')['default']
MaterialSymbolsInboxOutlineRounded: typeof import('~icons/material-symbols/inbox-outline-rounded')['default']
MaterialSymbolsKeyboardArrowDownRounded: typeof import('~icons/material-symbols/keyboard-arrow-down-rounded')['default']
MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default']
MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default']
@ -121,15 +126,20 @@ declare module '@vue/runtime-core' {
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiCircleMedium: typeof import('~icons/mdi/circle-medium')['default']
MdiClose: typeof import('~icons/mdi/close')['default']
MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiEditOutline: typeof import('~icons/mdi/edit-outline')['default']
MdiEye: typeof import('~icons/mdi/eye')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
@ -143,6 +153,8 @@ declare module '@vue/runtime-core' {
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']

6
packages/nc-gui/components/api-client/Params.vue

@ -23,7 +23,6 @@ const deleteParamRow = (i: number) => {
<table class="w-full nc-webhooks-params">
<thead class="h-8">
<tr>
<th></th>
<th>
<div class="text-left font-normal ml-2">Parameter Name</div>
</th>
@ -40,11 +39,6 @@ const deleteParamRow = (i: number) => {
<tbody>
<tr v-for="(paramRow, idx) in vModel" :key="idx" class="!h-2 overflow-hidden">
<td class="px-2 nc-hook-params-tab-checkbox">
<a-form-item class="form-item">
<a-checkbox v-model:checked="paramRow.enabled" />
</a-form-item>
</td>
<td class="px-2">
<a-form-item class="form-item">
<a-input v-model:value="paramRow.name" placeholder="Key" class="!rounded-lg" />

7
packages/nc-gui/components/cell/DateTimePicker.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk'
import {
ActiveCellInj,
CellClickHookInj,
@ -236,11 +237,16 @@ const clickHandler = () => {
}
cellClickHandler()
}
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
</script>
<template>
<a-date-picker
v-model:value="localState"
:disabled="isColDisabled"
:show-time="true"
:bordered="false"
class="!w-full !px-0 !border-none"
@ -251,7 +257,6 @@ const clickHandler = () => {
:input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`"
:open="readOnly || (localState && isPk) || isLockedMode ? false : open && (active || editable)"
:disabled="readOnly || (localState && isPk)"
@click="clickHandler"
@ok="open = !open"
>

162
packages/nc-gui/components/dashboard/Sidebar.vue

@ -1,144 +1,80 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useGlobal } from '#imports'
const router = useRouter()
const route = router.currentRoute
const workspaceStore = useWorkspace()
const { activeWorkspace, isWorkspaceOwnerOrCreator } = storeToRefs(workspaceStore)
const projectStore = useProject()
const { isSharedBase } = storeToRefs(projectStore)
const { navigateToWorkspaceSettings } = useWorkspace()
const { isUIAllowed } = useUIPermission()
const { isWorkspaceLoading } = storeToRefs(workspaceStore)
const dialogOpen = ref(false)
const openDialogKey = ref<string>('')
const dataSourcesState = ref<string>('')
const projectId = ref<string>()
const { isSharedBase } = storeToRefs(useProject())
const isCreateProjectOpen = ref(false)
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key || ''
dataSourcesState.value = dsState || ''
projectId.value = pId || ''
}
const treeViewDom = ref<HTMLElement>()
// todo:
const currentVersion = ref('')
const isTreeViewOnScrollTop = ref(false)
const isTreeViewOnScrollTop = ref(true)
const onTreeViewScrollTop = (onScrollTop: boolean) => {
isTreeViewOnScrollTop.value = !onScrollTop
const checkScrollTopMoreThanZero = () => {
if (treeViewDom.value) {
if (treeViewDom.value.scrollTop > 0) {
isTreeViewOnScrollTop.value = true
} else {
isTreeViewOnScrollTop.value = false
}
}
return false
}
const { appInfo } = useGlobal()
onMounted(() => {
treeViewDom.value?.addEventListener('scroll', checkScrollTopMoreThanZero)
})
const navigateToSettings = () => {
navigateToWorkspaceSettings()
}
onUnmounted(() => {
treeViewDom.value?.removeEventListener('scroll', checkScrollTopMoreThanZero)
})
</script>
<template>
<div
class="nc-sidebar flex flex-col bg-gray-50 outline-r-1 outline-gray-100 select-none"
class="nc-sidebar flex flex-col bg-gray-50 outline-r-1 outline-gray-100 select-none w-full h-full"
:style="{
outlineWidth: '1px',
height: isSharedBase ? '100%' : null,
}"
>
<div class="flex flex-col">
<div style="border-bottom-width: 1px" class="flex items-center px-1 nc-sidebar-header !border-0 py-1.25 pl-2">
<div class="flex flex-row flex-grow hover:bg-gray-100 pl-2 pr-1 py-0.5 rounded-md max-w-full">
<a
v-if="isSharedBase"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
href="https://github.com/nocodb/nocodb"
target="_blank"
>
<img width="25" alt="NocoDB" src="~/assets/img/icons/512x512.png" />
</a>
<DashboardSidebarHeader />
<WorkspaceMenu :workspace="activeWorkspace" :is-open="true">
<template #brandIcon>
<div
v-if="!isSharedBase"
v-e="['c:navbar:home']"
data-testid="nc-noco-brand-icon"
class="w-[29px] min-w-[29px] nc-noco-brand-icon"
>
<img width="25" class="mr-0" alt="NocoDB" src="~/assets/img/icons/512x512.png" />
</div>
</template>
</WorkspaceMenu>
</div>
</div>
<template v-if="!isSharedBase">
<div class="h-auto">
<div
v-if="isWorkspaceOwnerOrCreator"
role="button"
class="nc-sidebar-top-button"
data-testid="nc-sidebar-team-settings-btn"
@click="navigateToSettings"
>
<GeneralIcon icon="settings" class="!h-3.9" />
<div>Team & Settings</div>
</div>
<DashboardSidebarTopSection v-if="!isSharedBase" />
</div>
<div
ref="treeViewDom"
class="flex flex-col nc-scrollbar-sm-dark flex-grow"
:class="{
'border-t-1': !isSharedBase,
'border-transparent': !isTreeViewOnScrollTop,
'pt-0.25': isSharedBase,
}"
>
<div v-if="!isSharedBase" class="flex flex-row w-full justify-between items-center my-1.5 pl-4 pr-1.75">
<template v-if="!isWorkspaceLoading">
<div class="text-gray-500 font-medium">{{ $t('objects.projects') }}</div>
<WorkspaceCreateProjectBtn
v-if="isUIAllowed('projectCreate', false)"
v-model:is-open="isCreateProjectOpen"
modal
type="text"
class="!p-0 mx-1"
data-testid="nc-sidebar-create-project-btn"
:active-workspace-id="route.params.typeOrId"
size="xxsmall"
class="!hover:bg-gray-200 !hover-text-gray-800 !text-gray-600"
:centered="true"
data-testid="nc-sidebar-create-project-btn-small"
>
<div
class="gap-x-2 flex flex-row w-full items-center nc-sidebar-top-button !my-0 !mx-0"
:class="{
'bg-gray-100': isCreateProjectOpen,
}"
>
<MdiPlus class="!h-4" />
<div class="flex">{{ $t('title.newProj') }}</div>
</div>
<GeneralIcon icon="plus" class="text-lg leading-6" style="-webkit-text-stroke: 0.2px" />
</WorkspaceCreateProjectBtn>
</div>
<div class="w-full mt-2"></div>
</template>
<a-skeleton-input v-else :active="true" class="mt-0.5 !w-40 !h-4 !rounded overflow-hidden" />
</div>
<div class="text-gray-500 mx-5 font-medium mb-1.5">{{ $t('objects.projects') }}</div>
<div
class="w-full border-b-1"
:class="{
'border-gray-200': !isTreeViewOnScrollTop,
'border-transparent': isTreeViewOnScrollTop,
}"
></div>
</template>
<LazyDashboardTreeView v-if="!isWorkspaceLoading" />
</div>
<div v-if="!isSharedBase" style="height: var(--sidebar-bottom-height)">
<DashboardSidebarUserInfo />
</div>
<LazyDashboardTreeViewNew
class="flex-1"
:class="{
'nc-shared-base': isSharedBase,
}"
@create-base-dlg="toggleDialog(true, 'dataSources', undefined, projectId)"
@on-scroll-top="onTreeViewScrollTop"
/>
</div>
</template>
@ -146,8 +82,4 @@ const navigateToSettings = () => {
.nc-sidebar-top-button {
@apply flex flex-row mx-1 px-3.5 rounded-md items-center py-0.75 my-0.5 gap-x-2 hover:bg-gray-200 cursor-pointer;
}
:deep(.nc-shared-base.nc-treeview-container) {
@apply !h-full;
}
</style>

60
packages/nc-gui/components/dashboard/Sidebar/Header.vue

@ -0,0 +1,60 @@
<script setup lang="ts">
const workspaceStore = useWorkspace()
const projectStore = useProject()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { isSharedBase } = storeToRefs(projectStore)
const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore)
</script>
<template>
<div
class="flex items-center px-2 nc-sidebar-header py-1.2 w-full border-b-1 border-gray-200 group"
:data-workspace-title="activeWorkspace?.title"
style="height: var(--topbar-height)"
>
<div v-if="!isWorkspaceLoading" class="flex flex-row items-center w-full">
<WorkspaceMenu />
<div class="flex flex-grow min-w-1"></div>
<NcTooltip
class="flex opacity-0 group-hover:opacity-100 transition-opacity duration-50"
:class="{
'!opacity-100': !isLeftSidebarOpen,
}"
placement="bottom"
hide-on-click
>
<template #title>
{{
isLeftSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
</template>
<NcButton
type="text"
size="small"
class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !hover:bg-gray-200"
@click="isLeftSidebarOpen = !isLeftSidebarOpen"
>
<div class="flex items-center text-inherit">
<GeneralIcon
icon="doubleLeftArrow"
class="duration-150 transition-all !text-lg -mt-0.5"
:class="{
'transform rotate-180': !isLeftSidebarOpen,
}"
/>
</div>
</NcButton>
</NcTooltip>
</div>
<div v-else class="flex flex-row items-center w-full mt-0.25 ml-2.5 gap-x-3">
<a-skeleton-input :active="true" class="!w-6 !h-6 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-40 !h-6 !rounded overflow-hidden" />
</div>
</div>
</template>

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

@ -0,0 +1,86 @@
<script setup lang="ts">
const workspaceStore = useWorkspace()
const projectStore = useProject()
const { appInfo } = useGlobal()
const { isWorkspaceLoading, isWorkspaceOwnerOrCreator, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore)
const { navigateToWorkspaceSettings } = workspaceStore
const { isSharedBase } = storeToRefs(projectStore)
const isCreateProjectOpen = ref(false)
const navigateToSettings = () => {
// TODO: Handle cloud case properly
navigateToWorkspaceSettings()
// if (appInfo.value.baseHostName) {
// window.location.href = `https://app.${appInfo.value.baseHostName}/dashboard`
// } else {
// }
}
</script>
<template>
<template v-if="isWorkspaceLoading">
<div class="flex flex-col w-full gap-y-3.75 ml-3 mt-3.75">
<div v-if="appInfo.ee" class="flex flex-row items-center w-full gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-40 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex flex-row items-center w-full gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-40 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex flex-row items-center w-full gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-40 !h-4 !rounded overflow-hidden" />
</div>
</div>
</template>
<template v-else-if="!isSharedBase">
<div class="flex flex-col p-1 gap-y-0.5 mt-0.25">
<DashboardSidebarTopSectionHeader />
<NcButton
v-if="isWorkspaceOwnerOrCreator"
type="text"
size="small"
class="nc-sidebar-top-button"
data-testid="nc-sidebar-team-settings-btn"
:centered="false"
:class="{
'!text-brand-500 !bg-brand-50 !hover:bg-brand-50': isWorkspaceSettingsPageOpened,
'!hover:bg-gray-200': !isWorkspaceSettingsPageOpened,
}"
@click="navigateToSettings"
>
<div class="flex items-center gap-2">
<GeneralIcon icon="settings" class="!h-4" />
<div>Team & Settings</div>
</div>
</NcButton>
<WorkspaceCreateProjectBtn
v-model:is-open="isCreateProjectOpen"
modal
type="text"
class="nc-sidebar-top-button !hover:bg-gray-200"
data-testid="nc-sidebar-create-project-btn"
>
<div class="gap-x-2 flex flex-row w-full items-center !font-normal">
<GeneralIcon icon="plus" />
<div class="flex">{{ $t('title.newProj') }}</div>
</div>
</WorkspaceCreateProjectBtn>
</div>
</template>
</template>
<style lang="scss" scoped>
.nc-sidebar-top-button {
@apply w-full !rounded-md !font-normal !px-3;
}
</style>

0
packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index/[type]/[viewId]/[[viewTitle]]/[...slugs].vue → packages/nc-gui/components/dashboard/Sidebar/TopSection/Header.vue

188
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -0,0 +1,188 @@
<script lang="ts" setup>
import GithubButton from 'vue-github-button'
const { user, signOut, token, appInfo } = useGlobal()
const { clearWorkspaces } = useWorkspace()
const { leftSidebarState } = storeToRefs(useSidebarStore())
const { copy } = useCopy(true)
const name = computed(() => `${user.value?.firstname ?? ''} ${user.value?.lastname ?? ''}`.trim())
const isMenuOpen = ref(false)
const isAuthTokenCopied = ref(false)
const isLoggingOut = ref(false)
const logout = async () => {
isLoggingOut.value = true
try {
await signOut(false)
await clearWorkspaces()
await navigateTo('/signin')
} catch (e) {
console.error(e)
} finally {
isLoggingOut.value = false
}
}
const onCopy = async () => {
try {
await copy(token.value!)
isAuthTokenCopied.value = true
} catch (e: any) {
console.error(e)
message.error(e.message)
}
}
watch(isMenuOpen, () => {
if (isAuthTokenCopied.value) {
isAuthTokenCopied.value = false
}
})
watch(leftSidebarState, () => {
if (leftSidebarState.value === 'peekCloseEnd') {
isMenuOpen.value = false
}
})
// This is a hack to prevent github button error (prevents navigateTo if user is not signed in)
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
</script>
<template>
<div class="flex w-full flex-col p-1 border-t-1 border-gray-200 gap-y-2">
<NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64">
<div
class="flex flex-row py-2 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-10"
data-testid="nc-sidebar-userinfo"
>
<GeneralUserIcon />
<div class="flex truncate">
{{ name ? name : user?.email }}
</div>
<GeneralIcon icon="arrowUp" class="!min-w-5" />
</div>
<template #overlay>
<NcMenu>
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout">
<GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" />
<GeneralIcon v-else icon="signout" class="menu-icon" />
Log Out</NcMenuItem
>
<NcDivider />
<a href="https://docs.nocodb.com" target="_blank" class="!underline-transparent">
<NcMenuItem>
<GeneralIcon icon="help" class="menu-icon" />
Help Center</NcMenuItem
>
</a>
<NcDivider />
<a href="https://discord.gg/5RgZmkW" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper"
><GeneralIcon class="social-icon" icon="discord" />Join our Discord</NcMenuItem
>
</a>
<a href="https://www.reddit.com/r/NocoDB" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper"><GeneralIcon class="social-icon" icon="reddit" />/r/NocoDB</NcMenuItem>
</a>
<a href="https://twitter.com/nocodb" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper group"
><GeneralIcon class="text-gray-500 group-hover:text-gray-800" icon="twitter" />Twitter</NcMenuItem
>
</a>
<template v-if="!appInfo.ee">
<NcDivider />
<a-popover key="language" class="lang-menu !py-0" placement="rightBottom">
<NcMenuItem>
<GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" />
{{ $t('labels.language') }}
<div class="flex items-center text-gray-400 text-xs">(Community Translated)</div>
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" />
</NcMenuItem>
<template #content>
<div class="bg-white max-h-50vh scrollbar-thin-dull min-w-50 !overflow-auto">
<LazyGeneralLanguageMenu />
</div>
</template>
</a-popover>
</template>
<NcDivider />
<NcMenuItem @click="onCopy"
><GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black menu-icon" /><GeneralIcon
v-else
icon="copy"
class="menu-icon"
/>
<template v-if="isAuthTokenCopied"> Copied Auth Token </template>
<template v-else> Copy Auth Token </template>
</NcMenuItem>
<nuxt-link v-e="['c:navbar:user:email']" class="!no-underline" to="/account/tokens">
<NcMenuItem><GeneralIcon icon="settings" class="menu-icon" /> Account Settings</NcMenuItem>
</nuxt-link>
</NcMenu>
</template>
</NcDropdown>
<div v-if="appInfo.ee" class="text-gray-500 text-xs pl-3">© 2023 NocoDB. Inc</div>
<div v-else-if="isMounted" class="flex flex-col gap-y-1 pt-1">
<div class="flex items-start flex-row justify-center px-2 gap-2">
<GithubButton href="https://github.com/nocodb/nocodb" data-icon="octicon-star" data-show-count="true" data-size="large">
Star
</GithubButton>
</div>
<div class="flex items-start flex-row justify-center gap-2">
<GeneralJoinCloud class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.menu-icon {
@apply !min-h-4.5;
line-height: 1rem;
font-size: 1.125rem;
}
:deep(.ant-popover-inner-content) {
@apply !p-0 !rounded-md;
}
.social-icon {
// Make icon black and white
filter: grayscale(100%);
// Make icon red on hover
&:hover {
filter: grayscale(100%) invert(100%);
}
}
.social-icon-wrapper {
.nc-icon {
@apply mr-0.15;
}
&:hover {
.social-icon {
filter: none !important;
}
}
}
</style>

1475
packages/nc-gui/components/dashboard/TreeView.vue

File diff suppressed because it is too large Load Diff

0
packages/nc-gui/components/dashboard/TreeViewNew/AddNewTableNode.vue → packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue

0
packages/nc-gui/components/dashboard/TreeViewNew/BaseOptions.vue → packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue

30
packages/nc-gui/components/dashboard/TreeViewNew/ProjectNode.vue → packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -175,6 +175,8 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
if (!table) return
project.value.isExpanded = true
if (!activeKey.value || !activeKey.value.includes(`collapse-${baseId}`)) {
activeKey.value.push(`collapse-${baseId}`)
}
@ -200,10 +202,19 @@ const addNewProjectChildEntity = async () => {
if (isAddNewProjectChildEntityLoading.value) return
isAddNewProjectChildEntityLoading.value = true
const isProjectPopulated = projectsStore.isProjectPopulated(project.value.id!)
if (!isProjectPopulated && project.value.type === NcProjectType.DB) {
// We do not wait for tables api, so that add new table is seamless.
// Only con would be while saving table duplicate table name FE validation might not work
// If the table list api takes time to load before the table name validation
loadProjectTables(project.value.id!)
}
try {
openTableCreateDialog()
if (!project.value.isExpanded) {
if (!project.value.isExpanded && project.value.type !== NcProjectType.DB) {
project.value.isExpanded = true
}
} finally {
@ -246,7 +257,6 @@ const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, to
}
if (!isProjectPopulated) {
await loadProject(project.id!)
await loadProjectTables(project.id!)
}
@ -375,7 +385,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@click="onProjectClick(project, true, true)"
>
<PhTriangleFill
class="absolute top-2.25 left-2 invisible group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90"
class="absolute top-2.25 left-2 group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90"
:class="{ '!rotate-180': project.isExpanded, '!visible': isOptionsOpen }"
/>
</div>
@ -444,7 +454,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<a-menu-item @click="enableEditMode">
<div class="nc-project-menu-item group">
<GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.edit') }}
{{ $t('general.rename') }}
</div>
</a-menu-item>
@ -458,7 +468,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<a-menu-item v-if="isUIAllowed('duplicateProject', true, projectRole)" @click="duplicateProject(project)">
<div class="nc-menu-item-wrapper">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('objects.project') }}
{{ $t('general.duplicate') }}
</div>
</a-menu-item>
@ -498,7 +508,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
</a-menu-item>
<template v-if="project.bases && project.bases[0]">
<DashboardTreeViewNewBaseOptions v-model:project="project" :base="project.bases[0]" />
<DashboardTreeViewBaseOptions v-model:project="project" :base="project.bases[0]" />
<a-menu-divider />
</template>
@ -540,7 +550,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<div class="flex-1 overflow-y-auto overflow-x-hidden flex flex-col" :class="{ 'mb-[20px]': isSharedBase }">
<div v-if="project?.bases?.[0]?.enabled" class="flex-1">
<div class="transition-height duration-200">
<DashboardTreeViewNewTableList :project="project" :base-index="0" />
<DashboardTreeViewTableList :project="project" :base-index="0" />
</div>
</div>
@ -627,7 +637,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
</a-menu-item>
<DashboardTreeViewNewBaseOptions v-model:project="project" :base="base" />
<DashboardTreeViewBaseOptions v-model:project="project" :base="base" />
</a-menu>
</template>
</a-dropdown>
@ -652,7 +662,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
:key="`sortable-${base.id}-${base.id && base.id in keys ? keys[base.id] : '0'}`"
:nc-base="base.id"
>
<DashboardTreeViewNewTableList :project="project" :base-index="baseIndex" />
<DashboardTreeViewTableList :project="project" :base-index="baseIndex" />
</div>
</a-collapse-panel>
</a-collapse>
@ -726,7 +736,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
}
:deep(.ant-collapse-header) {
@apply !mx-0 !pl-8.75 !pr-1 !py-0.75 hover:bg-gray-100 !rounded-md;
@apply !mx-0 !pl-8.75 !pr-1 !py-0.75 hover:bg-gray-200 !rounded-md;
}
:deep(.ant-collapse-header:hover .nc-sidebar-base-node-btns) {

0
packages/nc-gui/components/dashboard/TreeViewNew/ProjectWrapper.vue → packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue

0
packages/nc-gui/components/dashboard/TreeViewNew/TableList.vue → packages/nc-gui/components/dashboard/TreeView/TableList.vue

17
packages/nc-gui/components/dashboard/TreeViewNew/TableNode.vue → packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -68,8 +68,11 @@ const setIcon = async (icon: string, table: TableType) => {
// Todo: temp
const { isSharedBase } = useProject()
// const isMultiBase = computed(() => project.bases && project.bases.length > 1)
const canUserEditEmote = computed(() => {
return isUIAllowed('tableIconCustomisation', false, projectRole?.value)
})
</script>
<template>
@ -98,16 +101,22 @@ const { isSharedBase } = useProject()
<template #title>{{ table.table_name }}</template>
<div class="table-context flex items-center gap-1 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div class="flex items-center nc-table-icon" @click.stop>
<div
class="flex items-center nc-table-icon"
:class="{
'pointer-events-none': !canUserEditEmote,
}"
@click.stop
>
<LazyGeneralEmojiPicker
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!isUIAllowed('tableIconCustomisation', false, projectRole)"
:readonly="!canUserEditEmote"
@emoji-selected="setIcon($event, table)"
>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ 'Change icon' }}
</template>

86
packages/nc-gui/components/dashboard/TreeViewNew/index.vue → packages/nc-gui/components/dashboard/TreeView/index.vue

@ -2,7 +2,6 @@
import type { TableType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import GithubButton from 'vue-github-button'
import ProjectWrapper from './ProjectWrapper.vue'
import type { TabType } from '#imports'
@ -28,10 +27,6 @@ import {
import { useRouter } from '#app'
const emit = defineEmits<{
(event: 'onScrollTop', type: boolean): void
}>()
const { isUIAllowed } = useUIPermission()
const { addTab } = useTabs()
@ -48,8 +43,12 @@ const { createProject: _createProject } = projectsStore
const { projects, projectsList, activeProjectId } = storeToRefs(projectsStore)
const { isWorkspaceLoading } = storeToRefs(useWorkspace())
const { openTable } = useTablesStore()
const projectCreateDlg = ref(false)
const projectStore = useProject()
const { loadTables } = projectStore
@ -184,6 +183,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
}
break
}
// ALT + D
case 68: {
e.stopPropagation()
projectCreateDlg.value = true
break
}
}
}
})
@ -203,19 +208,6 @@ provide(TreeViewInj, {
useEventListener(document, 'contextmenu', handleContext, true)
const treeViewDom = ref<HTMLElement>()
const checkScrollTopMoreThanZero = () => {
if (treeViewDom.value) {
if (treeViewDom.value.scrollTop > 0) {
emit('onScrollTop', true)
} else {
emit('onScrollTop', false)
}
}
return false
}
const scrollTableNode = () => {
const activeTableDom = document.querySelector(`.nc-treeview [data-table-id="${_activeTable.value?.id}"]`)
if (!activeTableDom) return
@ -256,56 +248,24 @@ watch(
immediate: true,
},
)
onMounted(() => {
treeViewDom.value?.addEventListener('scroll', checkScrollTopMoreThanZero)
})
onUnmounted(() => {
treeViewDom.value?.removeEventListener('scroll', checkScrollTopMoreThanZero)
})
</script>
<template>
<div class="nc-treeview-container flex flex-col justify-between select-none">
<div ref="treeViewDom" mode="inline" class="nc-treeview pb-0.5 flex-grow min-h-50 overflow-x-hidden">
<div mode="inline" class="nc-treeview pb-0.5 flex-grow min-h-50 overflow-x-hidden">
<template v-if="projectsList?.length">
<ProjectWrapper
v-for="project of projectsList"
:key="project.id"
:project-role="project.project_role || project.workspace_role"
:project="project"
>
<DashboardTreeViewNewProjectNode />
<ProjectWrapper v-for="project of projectsList" :key="project.id" :project-role="project.project_role" :project="project">
<DashboardTreeViewProjectNode />
</ProjectWrapper>
</template>
<WorkspaceEmptyPlaceholder v-else />
</div>
<div class="flex items-start flex-row justify-center px-2 gap-2">
<GithubButton
class="ml-2"
href="https://github.com/nocodb/nocodb"
data-icon="octicon-star"
data-show-count="true"
data-size="large"
>
Star
</GithubButton>
</div>
<div class="flex items-start flex-row justify-center px-2 pt-1 pb-1.5 gap-2">
<GeneralJoinCloud class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<WorkspaceEmptyPlaceholder v-else-if="!isWorkspaceLoading" />
</div>
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" />
</div>
</template>
<style scoped lang="scss">
.nc-treeview-container {
height: calc(100% - var(--sidebar-top-height));
}
.nc-treeview-footer-item {
@apply cursor-pointer px-4 py-2 flex items-center hover:bg-gray-200/20 text-xs text-current;
}
@ -413,20 +373,4 @@ onUnmounted(() => {
}
}
}
.nc-treeview {
overflow-y: overlay;
&::-webkit-scrollbar {
width: 3px;
}
&::-webkit-scrollbar-track {
@apply bg-inherit;
}
&::-webkit-scrollbar-thumb {
@apply bg-scrollbar;
}
&::-webkit-scrollbar-thumb:hover {
@apply bg-scrollbar-hover;
}
}
</style>

5
packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue

@ -264,16 +264,17 @@ onMounted(async () => {
</template>
<div class="flex flex-row space-x-4 justify-center mt-4">
<a-button
<NcButton
v-for="(action, i) in plugin.formDetails.actions"
:key="i"
class="!px-5"
:loading="loadingAction === action.key"
:type="action.key === Action.Save ? 'primary' : 'default'"
:disabled="!!loadingAction"
@click="doAction(action.key)"
>
{{ action.label }}
</a-button>
</NcButton>
</div>
</a-form>
</div>

1
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -379,6 +379,7 @@ watch(
<div class="create-base bg-white relative flex flex-col justify-center gap-2 w-full">
<h1 class="prose-2xl font-bold self-start mb-4 flex items-center gap-2">
New Base
<DashboardSettingsDataSourcesInfo />
<span class="flex-grow"></span>
</h1>

4
packages/nc-gui/components/dlg/AirtableImport.vue

@ -25,9 +25,9 @@ const { modelValue, baseId } = defineProps<{
const emit = defineEmits(['update:modelValue'])
const { appInfo } = useGlobal()
const { $api } = useNuxtApp()
const baseURL = appInfo.value.ncSiteUrl
const baseURL = $api.instance.defaults.baseURL
const { $state, $jobs } = useNuxtApp()

22
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -33,8 +33,8 @@ const optionsToExclude = computed(() => {
const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
try {
isLoading.value = true
// pick a random color from array and assign to project
const color = projectThemeColors[Math.floor(Math.random() * 1000) % projectThemeColors.length]
const tcolor = tinycolor(color)
@ -58,18 +58,28 @@ const _duplicate = async () => {
props.onOk(jobData as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
dialogShow.value = false
}
isLoading.value = false
dialogShow.value = false
}
onKeyStroke('Enter', () => {
// should only trigger this when our modal is open
if (dialogShow.value) {
_duplicate()
}
})
const isEaster = ref(false)
</script>
<template>
<GeneralModal v-if="project" v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-project-duplicate">
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">{{ $t('general.duplicate') }}</div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.project') }}
</div>
<div class="mt-4">Are you sure you want to duplicate the `{{ project.title }}` project?</div>
@ -85,9 +95,7 @@ const isEaster = ref(false)
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton>
<NcButton key="submit" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton>
</div>
</GeneralModal>
</template>
<style scoped lang="scss"></style>

54
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -32,55 +32,59 @@ const optionsToExclude = computed(() => {
const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
try {
isLoading.value = true
const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, { options: optionsToExclude.value })
props.onOk(jobData as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
dialogShow.value = false
}
isLoading.value = false
dialogShow.value = false
}
onKeyStroke('Enter', () => {
// should only trigger this when our modal is open
if (dialogShow.value) {
_duplicate()
}
})
const isEaster = ref(false)
</script>
<template>
<a-modal
<GeneralModal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
centered
wrap-class-name="nc-modal-table-duplicate"
:footer="null"
:closable="false"
class="!w-[30rem]"
@keydown.esc="dialogShow = false"
>
<div>
<div class="text-base font-medium self-center mb-4" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ table.title }}
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div>
<div class="flex flex-col gap-y-2">
<div class="flex flex-row gap-x-2 items-center">
<a-switch v-model:checked="options.includeData" />
Include Data
</div>
<div class="flex flex-row gap-x-2 items-center">
<a-switch v-model:checked="options.includeViews" />
Include Views
</div>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">Include hooks</a-checkbox>
<div class="mt-4">Are you sure you want to duplicate the `{{ table.title }}` table?</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">Include data</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">Include views</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">Include webhooks</a-checkbox>
</div>
</div>
<div class="flex flex-row justify-end gap-x-2 mt-8">
<a-button key="back" size="middle" class="!rounded-md" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="middle" type="primary" class="!rounded-md" :loading="isLoading" @click="_duplicate"
>{{ $t('general.duplicate') }}
</a-button>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton>
</div>
</a-modal>
</GeneralModal>
</template>
<style scoped lang="scss"></style>

7
packages/nc-gui/components/dlg/share-and-collaborate/View.vue

@ -155,11 +155,8 @@ watch(showShareModal, (val) => {
</div>
<div class="share-base">
<div class="flex flex-row items-center gap-x-2 px-4 pt-3 pb-3 select-none">
<component
:is="viewIcons[view?.type]?.icon"
class="nc-view-icon group-hover"
:style="{ color: viewIcons[view?.type]?.color }"
/>
<GeneralProjectIcon :type="project.type" class="nc-view-icon group-hover" />
<div>Share Base</div>
<div
class="max-w-79/100 ml-2 px-2 py-0.5 rounded-md bg-gray-100 capitalize text-ellipsis overflow-hidden"

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

@ -80,9 +80,9 @@ const showClearButton = computed(() => {
<template>
<a-dropdown v-model:visible="isOpen" trigger="click" :disabled="readonly">
<div
class="flex flex-row justify-center items-center select-none cursor-pointer rounded-md"
class="flex flex-row justify-center items-center select-none rounded-md"
:class="{
'hover:bg-gray-500 hover:bg-opacity-15': !readonly,
'hover:bg-gray-500 hover:bg-opacity-15 cursor-pointer': !readonly,
'bg-gray-500 bg-opacity-15': isOpen,
'h-6 w-6 text-lg': size === 'small',
'h-8 w-8 text-xl': size === 'medium',

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

@ -12,8 +12,8 @@ const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
const logout = async () => {
await signOut()
await navigateTo('/signin')
await signOut(false)
navigateTo('/signin')
}
</script>

53
packages/nc-gui/components/general/OpenLeftSidebarBtn.vue

@ -0,0 +1,53 @@
<script lang="ts" setup>
const { isLeftSidebarOpen: _isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const isLeftSidebarOpen = ref(_isLeftSidebarOpen.value)
watch(_isLeftSidebarOpen, (val) => {
if (val) {
isLeftSidebarOpen.value = true
} else {
setTimeout(() => {
isLeftSidebarOpen.value = false
}, 300)
}
})
const onClick = () => {
if (_isLeftSidebarOpen.value) return
_isLeftSidebarOpen.value = !_isLeftSidebarOpen.value
}
</script>
<template>
<NcTooltip
placement="topLeft"
hide-on-click
class="transition-all duration-100"
:class="{
'!w-0 !opacity-0': isLeftSidebarOpen,
'!w-8 !opacity-100': !isLeftSidebarOpen,
}"
>
<template #title>
{{
isLeftSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
</template>
<NcButton
type="text"
size="small"
class="nc-sidebar-left-toggle-icon !text-gray-600 !hover:text-gray-800"
:class="{
'invisible !w-0': isLeftSidebarOpen,
}"
@click="onClick"
>
<div class="flex items-center text-inherit">
<GeneralIcon icon="doubleRightArrow" class="duration-150 transition-all !text-lg -mt-0.25" />
</div>
</NcButton>
</NcTooltip>
</template>

8
packages/nc-gui/components/general/ReleaseInfo.vue

@ -3,7 +3,7 @@ import { computed, extractSdkResponseErrorMsg, message, onMounted, useGlobal, us
const { $api } = useNuxtApp()
const { currentVersion, latestRelease, hiddenRelease } = useGlobal()
const { currentVersion, latestRelease, hiddenRelease, appInfo } = useGlobal()
const releaseAlert = computed({
get() {
@ -41,14 +41,14 @@ onMounted(async () => await fetchReleaseInfo())
</script>
<template>
<div v-if="releaseAlert" class="flex items-center">
<div v-if="releaseAlert && !appInfo.ee" class="flex items-center">
<a-dropdown :trigger="['click']" placement="bottom" overlay-class-name="nc-dropdown-upgrade-menu">
<a-button class="!bg-primary !border-none">
<NcButton class="!bg-primary !border-none !mr-3" size="small">
<div class="flex gap-1 items-center text-white">
<span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span>
<mdi-menu-down />
</div>
</a-button>
</NcButton>
<template #overlay>
<div class="mt-1 bg-white shadow-lg !border">

51
packages/nc-gui/components/general/UserIcon.vue

@ -0,0 +1,51 @@
<script lang="ts" setup>
import type { UserType } from 'nocodb-sdk'
const props = defineProps<{
hideLabel?: boolean
size?: 'small' | 'medium'
}>()
const { user } = useGlobal()
const backgroundColor = computed(() => (user.value?.id ? stringToColour(user.value?.id) : '#FFFFFF'))
const size = computed(() => props.size || 'medium')
const firstName = computed(() => user.value?.firstname ?? '')
const lastName = computed(() => user.value?.lastname ?? '')
const email = computed(() => user.value?.email ?? '')
const usernameInitials = computed(() => {
if (firstName.value && lastName.value) {
return firstName.value[0] + lastName.value[0]
} else if (firstName.value) {
return firstName.value[0] + (firstName.value.length > 1 ? firstName.value[1] : '')
} else if (lastName.value) {
return lastName.value[0] + (lastName.value.length > 1 ? lastName.value[1] : '')
} else {
return email.value[0] + email.value[1]
}
})
</script>
<template>
<div
class="flex nc-user-avatar"
:class="{
'min-w-4 min-h-4': size === 'small',
'min-w-6 min-h-6': size === 'medium',
}"
:style="{ backgroundColor }"
>
<template v-if="!props.hideLabel">
{{ usernameInitials }}
</template>
</div>
</template>
<style lang="scss" scoped>
.nc-user-avatar {
@apply rounded-full text-xs flex items-center justify-center text-white uppercase;
}
</style>

11
packages/nc-gui/components/general/ViewIcon.vue

@ -23,11 +23,14 @@ const viewMeta = toRef(props, 'meta')
v-else-if="viewMeta?.type"
class="nc-view-icon group-hover"
:style="{
'color': !props.ignoreColor ? viewIcons[viewMeta.type]?.color : undefined,
'fontWeight': 500,
'-webkit-text-stroke': !props.ignoreColor ? `0.5px ${viewIcons[viewMeta.type]?.color}` : '0.5px',
color: !props.ignoreColor ? viewIcons[viewMeta.type]?.color : undefined,
fontWeight: 500,
}"
/>
</template>
<style scoped></style>
<style>
.nc-view-icon {
font-size: 1.05rem;
}
</style>

37
packages/nc-gui/components/general/WorkspaceIcon.vue

@ -0,0 +1,37 @@
<script lang="ts" setup>
import type { WorkspaceType } from 'nocodb-sdk'
const props = defineProps<{
workspace: WorkspaceType | undefined
hideLabel?: boolean
size?: 'small' | 'medium' | 'large'
}>()
const workspaceColor = computed(() =>
props.workspace ? props.workspace.meta?.color || stringToColour(props.workspace.id!) : undefined,
)
const size = computed(() => props.size || 'medium')
</script>
<template>
<div
class="flex nc-workspace-avatar"
:class="{
'min-w-4 w-4 h-4 rounded': size === 'small',
'min-w-6 w-6 h-6 rounded-md': size === 'medium',
'min-w-10 w-10 h-10 rounded-lg !text-base': size === 'large',
}"
:style="{ backgroundColor: workspaceColor }"
>
<template v-if="!props.hideLabel">
{{ props.workspace?.title?.slice(0, 2) }}
</template>
</div>
</template>
<style lang="scss" scoped>
.nc-workspace-avatar {
@apply text-xs flex items-center justify-center text-white uppercase;
}
</style>

2
packages/nc-gui/components/general/language/index.vue

@ -9,7 +9,7 @@
</div>
<template #overlay>
<a-menu class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0 rounded">
<a-menu class="nc-scrollbar-dark-md min-w-50 max-h-90vh overflow-auto !p-1 m-1 rounded-md">
<GeneralLanguageMenu />
</a-menu>
</template>

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

@ -1,6 +1,4 @@
<script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps<{
color: string
}>()
@ -8,26 +6,22 @@ const props = defineProps<{
<template>
<div
class="badge-color"
class="border-1 h-6 rounded-md px-1"
:class="{
'border-purple-500': props.color === 'purple',
'bg-purple-100': props.color === 'purple',
'border-blue-500': props.color === 'blue',
'bg-blue-100': props.color === 'blue',
'border-green-500': props.color === 'green',
'bg-green-100': props.color === 'green',
'border-orange-500': props.color === 'orange',
'bg-orange-100': props.color === 'orange',
'border-yellow-500': props.color === 'yellow',
'bg-yellow-100': props.color === 'yellow',
'border-purple-500 bg-purple-100': props.color === 'purple',
'border-blue-500 bg-blue-100': props.color === 'blue',
'border-green-500 bg-green-100': props.color === 'green',
'border-orange-500 bg-orange-100': props.color === 'orange',
'border-yellow-500 bg-yellow-100': props.color === 'yellow',
'border-gray-300': !props.color,
}"
>
<slot></slot>
<slot />
</div>
</template>
<style>
.badge-color {
@apply mt-1 border w-23 h-6 rounded-lg;
@apply mt-1 w-23 border h-6 rounded-lg;
}
</style>

17
packages/nc-gui/components/nc/Button.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { ButtonType } from 'ant-design-vue/lib/button'
import { useSlots } from 'vue'
import type { NcButtonSize } from '~/lib'
/**
* @description
@ -17,7 +18,7 @@ interface Props {
loading?: boolean
disabled?: boolean
type?: ButtonType | 'danger' | undefined
size?: 'xsmall' | 'small' | 'medium'
size?: NcButtonSize
centered?: boolean
}
@ -74,6 +75,7 @@ const onBlur = () => {
small: size === 'small',
medium: size === 'medium',
xsmall: size === 'xsmall',
xxsmall: size === 'xxsmall',
focused: isFocused,
}"
@focus="onFocus"
@ -91,6 +93,7 @@ const onBlur = () => {
<slot v-else name="icon" />
<div
class="flex flex-row items-center"
:class="{
'font-medium': type === 'primary' || type === 'danger',
}"
@ -120,7 +123,7 @@ const onBlur = () => {
}
.nc-button.ant-btn {
@apply rounded-lg font-medium;
@apply rounded-lg font-medium;
}
.nc-button.ant-btn.small {
@ -132,7 +135,11 @@ const onBlur = () => {
}
.nc-button.ant-btn.xsmall {
@apply p-0.25 h-6.25 w-6.25;
@apply p-0.25 h-6.25 min-w-6.25 rounded-md;
}
.nc-button.ant-btn.xxsmall {
@apply p-0 h-6 min-w-6 rounded-md;
}
.nc-button.ant-btn[disabled] {
@ -160,7 +167,7 @@ const onBlur = () => {
@apply bg-white border-1 border-gray-200 text-gray-700;
&:hover {
@apply bg-gray-50;
@apply bg-gray-100;
}
}
@ -175,7 +182,7 @@ const onBlur = () => {
.nc-button.ant-btn-text {
box-shadow: none;
@apply bg-transparent border-0 text-gray-700 hover:bg-gray-50;
@apply bg-transparent border-0 text-gray-700 hover:bg-gray-100;
&:focus {
box-shadow: none;

9
packages/nc-gui/components/nc/Divider.vue

@ -0,0 +1,9 @@
<template>
<a-divider class="nc-divider" />
</template>
<style lang="scss">
.nc-divider.ant-divider {
@apply !my-1;
}
</style>

24
packages/nc-gui/components/nc/Dropdown.vue

@ -1,14 +1,18 @@
<script lang="ts" setup>
import { onKeyStroke } from '#imports'
const props = withDefaults(
defineProps<{
trigger?: Array<'click' | 'hover' | 'contextmenu'>
visible?: boolean | undefined
overlayClassName?: string | undefined
autoClose?: boolean
}>(),
{
trigger: () => ['click'],
visible: undefined,
overlayClassName: undefined,
autoClose: true,
},
)
@ -18,8 +22,10 @@ const trigger = toRef(props, 'trigger')
const overlayClassName = toRef(props, 'overlayClassName')
const autoClose = computed(() => props.autoClose)
const overlayClassNameComputed = computed(() => {
let className = 'nc-dropdown bg-white rounded-2xl border-1 border-gray-100 shadow-md overflow-hidden'
let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-100 shadow-md overflow-hidden'
if (overlayClassName.value) {
className += ` ${overlayClassName.value}`
}
@ -27,6 +33,20 @@ const overlayClassNameComputed = computed(() => {
})
const visible = useVModel(props, 'visible', emits)
onKeyStroke('Escape', () => {
if (visible.value && autoClose.value) {
visible.value = false
}
})
const overlayWrapperDomRef = ref<HTMLElement | null>(null)
onClickOutside(overlayWrapperDomRef, () => {
if (!autoClose.value) return
visible.value = false
})
</script>
<template>
@ -39,7 +59,7 @@ const visible = useVModel(props, 'visible', emits)
<slot />
<template #overlay>
<slot name="overlay" />
<slot ref="overlayWrapperDomRef" name="overlay" />
</template>
</a-dropdown>
</template>

19
packages/nc-gui/components/nc/Menu.vue

@ -0,0 +1,19 @@
<script lang="ts" setup>
const props = defineProps<{
selectable?: boolean | undefined
}>()
const selectable = computed(() => props.selectable ?? false)
</script>
<template>
<a-menu class="nc-menu" :selectable="selectable">
<slot />
</a-menu>
</template>
<style lang="scss">
.nc-menu {
@apply bg-white !rounded-md !py-1;
}
</style>

17
packages/nc-gui/components/nc/MenuItem.vue

@ -0,0 +1,17 @@
<template>
<a-menu-item class="nc-menu-item">
<div class="nc-menu-item-inner">
<slot />
</div>
</a-menu-item>
</template>
<style lang="scss">
.nc-menu-item {
@apply !py-2 font-normal text-sm;
}
.nc-menu-item-inner {
@apply flex flex-row items-center gap-x-2.25;
}
</style>

4
packages/nc-gui/components/nc/Modal.vue

@ -55,6 +55,8 @@ const height = computed(() => {
})
const visible = useVModel(props, 'visible', emits)
const slots = useSlots()
</script>
<template>
@ -76,7 +78,7 @@ const visible = useVModel(props, 'visible', emits)
maxHeight: height,
}"
>
<div class="flex pb-2 mb-2 text-lg font-medium border-b-1 border-gray-100">
<div v-if="slots.header" class="flex pb-2 mb-2 text-lg font-medium border-b-1 border-gray-100">
<slot name="header" />
</div>

18
packages/nc-gui/components/nc/Tooltip.vue

@ -14,13 +14,19 @@ interface Props {
hideOnClick?: boolean
}
const { modifierKey, tooltipStyle, disabled, hideOnClick } = defineProps<Props>()
const props = defineProps<Props>()
const modifierKey = computed(() => props.modifierKey)
const tooltipStyle = computed(() => props.tooltipStyle)
const disabled = computed(() => props.disabled)
const hideOnClick = computed(() => props.hideOnClick)
const placement = computed(() => props.placement ?? 'top')
const el = ref()
const showTooltip = controlledRef(false, {
onBeforeChange: (shouldShow) => {
if (shouldShow && disabled) return false
if (shouldShow && disabled.value) return false
},
})
@ -31,7 +37,7 @@ const attrs = useAttrs()
const isKeyPressed = ref(false)
onKeyStroke(
(e) => e.key === modifierKey,
(e) => e.key === modifierKey.value,
(e) => {
e.preventDefault()
@ -45,7 +51,7 @@ onKeyStroke(
)
onKeyStroke(
(e) => e.key === modifierKey,
(e) => e.key === modifierKey.value,
(e) => {
e.preventDefault()
@ -55,7 +61,7 @@ onKeyStroke(
{ eventName: 'keyup' },
)
watch([isHovering, () => modifierKey, () => disabled], ([hovering, key, isDisabled]) => {
watch([isHovering, () => modifierKey.value, () => disabled.value], ([hovering, key, isDisabled]) => {
if (!hovering || isDisabled) {
showTooltip.value = false
return
@ -85,7 +91,7 @@ const divStyles = computed(() => ({
}))
const onClick = () => {
if (hideOnClick && showTooltip.value) {
if (hideOnClick.value && showTooltip.value) {
showTooltip.value = false
}
}

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

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { OrderedProjectRoles, RoleColors, RoleLabels } from 'nocodb-sdk'
import type { ProjectRoles, WorkspaceUserType } from 'nocodb-sdk'
import type { WorkspaceUserType } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles, RoleColors, RoleLabels } from 'nocodb-sdk'
import InfiniteLoading from 'v3-infinite-loading'
import { storeToRefs, stringToColour, timeAgo, useGlobal } from '#imports'
import { isEeUI, storeToRefs, stringToColour, timeAgo, useGlobal } from '#imports'
const { user } = useGlobal()
const projectsStore = useProjects()
@ -35,7 +35,7 @@ const loadCollaborators = async () => {
...user,
projectRoles: user.roles,
// TODO: Remove this hack and make the values consistent with the backend
roles: user.roles ?? (RoleLabels[user.workspace_roles as string] as string)?.toLowerCase(),
roles: user.roles ?? (RoleLabels[user.workspace_roles as string] as string)?.toLowerCase() ?? ProjectRoles.NO_ACCESS,
})),
]
} catch (e: any) {
@ -74,7 +74,7 @@ onMounted(async () => {
const updateCollaborator = async (collab, roles) => {
try {
if (!roles) {
if (!roles || roles === 'inherit' || (roles === ProjectRoles.NO_ACCESS && !isEeUI)) {
await removeProjectUser(activeProjectId.value!, collab)
collab.projectRoles = null
} else if (collab.projectRoles) {
@ -187,8 +187,7 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
class="w-35 !rounded px-1"
:virtual="true"
:placeholder="$t('labels.noAccess')"
:disabled="collab.id === user?.id"
allow-clear
:disabled="collab.id === user?.id || (collab.roles && !accessibleRoles.includes(collab.roles))"
@change="(value) => updateCollaborator(collab, value)"
>
<template #suffixIcon>
@ -211,6 +210,11 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
</NcBadge>
</a-select-option>
</template>
<a-select-option v-if="isEeUI" value="inherit">
<NcBadge color="white">
<p class="badge-text">Inherit</p>
</NcBadge>
</a-select-option>
</NcSelect>
</div>
</div>
@ -259,4 +263,8 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
:deep(.nc-collaborator-role-select .ant-select-selector) {
@apply !rounded;
}
:deep(.ant-select-selection-item) {
@apply mt-0.75;
}
</style>

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

@ -4,7 +4,7 @@ import dayjs from 'dayjs'
const { activeTables } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore()
const { openedProject, roles } = storeToRefs(useProjects())
const { openedProject } = storeToRefs(useProjects())
const { isUIAllowed } = useUIPermission()

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

@ -111,7 +111,7 @@ const copyUrl = async () => {
class="!max-w-130 !rounded"
/>
<a-select v-model:value="inviteData.roles" class="min-w-30 !rounded px-1" data-testid="roles">
<NcSelect v-model:value="inviteData.roles" class="min-w-30 !rounded px-1" data-testid="roles">
<template #suffixIcon>
<MdiChevronDown />
</template>
@ -140,7 +140,7 @@ const copyUrl = async () => {
<p class="badge-text">{{ role }}</p>
</NcBadge>
</a-select-option>
</a-select>
</NcSelect>
<a-button
type="primary"
@ -162,10 +162,14 @@ const copyUrl = async () => {
<style scoped>
.badge-text {
@apply text-[14px] pt-1 text-center;
@apply text-[14px] pt-1 text-center font-medium first-letter:uppercase;
}
:deep(.ant-select .ant-select-selector) {
@apply rounded;
}
:deep(.ant-select-selection-item) {
@apply mt-0.75;
}
</style>

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

@ -57,10 +57,11 @@ watch(
<template>
<div class="h-full nc-project-view">
<div
class="flex flex-row pl-5 pr-3 border-b-1 border-gray-200 justify-between w-full"
class="flex flex-row pl-2 pr-2 border-b-1 border-gray-200 justify-between w-full"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
>
<div class="flex flex-row items-center gap-x-4">
<GeneralOpenLeftSidebarBtn />
<GeneralProjectIcon :type="openedProject?.type" />
<div class="flex font-medium text-base capitalize">
{{ openedProject?.title }}

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

@ -39,7 +39,6 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj)
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const { isViewDataLoading } = storeToRefs(useViewsStore())
const { isSqlView, xWhere } = useSmartsheetStoreOrThrow()
const expandedFormDlg = ref(false)
const expandedFormRow = ref<RowType>()
@ -55,7 +54,7 @@ const {
addEmptyRow,
deleteRow,
navigateToSiblingRow,
} = useViewData(meta, view, xWhere)
} = useViewData(meta, view)
provide(IsFormInj, ref(false))
provide(IsGalleryInj, ref(true))
@ -86,6 +85,8 @@ const isRowEmpty = (record: any, col: any) => {
return Array.isArray(val) && val.length === 0
}
const { isSqlView } = useSmartsheetStoreOrThrow()
const { isUIAllowed } = useUIPermission()
const hasEditPermission = computed(() => isUIAllowed('xcDatatableEditable'))
// TODO: extract this code (which is duplicated in grid and gallery) into a separate component

43
packages/nc-gui/components/smartsheet/Pagination.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import type { PaginatedType } from 'nocodb-sdk'
import SidebarIcon from '~icons/nc-icons/sidebar'
import { IsGroupByInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports'
import type { Language } from '#imports'
@ -66,24 +65,6 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : ''
}${extraStyle}`"
>
<NcTooltip v-if="!isPublic && hideSidebars !== true" class="ml-2" placement="topLeft" hide-on-click>
<template #title>
{{
isLeftSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
</template>
<div
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) hover:(bg-gray-100 border-gray-200) border-gray-200 group flex items-center justify-center rounded-md h-full px-1.75 h-7 cursor-pointer text-gray-500 hover:text-gray-700"
:class="{
'bg-gray-100': !isLeftSidebarOpen,
}"
@click="isLeftSidebarOpen = !isLeftSidebarOpen"
>
<SidebarIcon class="cursor-pointer transform transition-transform duration-500 rounded-md rotate-180" />
</div>
</NcTooltip>
<slot name="add-record" />
<div class="flex-1">
<span
@ -117,7 +98,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</div>
</template>
<div class="flex-1 text-right pr-2">
<div class="flex-1 text-right">
<span
v-if="alignCountOnRight && count !== null && count !== Infinity"
class="caption nc-grid-row-count mr-2.5 text-gray-500 text-xs"
@ -126,28 +107,6 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
{{ count }} {{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span>
</div>
<NcTooltip v-if="!isPublic && hideSidebars !== true" placement="topRight" hide-on-click>
<template #title>
{{
isRightSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
</template>
<div
class="flex flex-row items-center justify-center !rounded-md !p-1.75 border-gray-100 cursor-pointer bg-white hover:bg-gray-100 text-gray-500 hover:text-gray-700 nc-sidebar-right-toggle-icon"
:class="{
'!bg-gray-100': !isRightSidebarOpen,
}"
type="ghost"
@click="isRightSidebarOpen = !isRightSidebarOpen"
>
<SidebarIcon class="w-4 h-4" />
</div>
</NcTooltip>
<div class="w-2"></div>
</div>
</template>

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

@ -12,12 +12,21 @@ const { isMobileMode } = useGlobal()
const { isUIAllowed } = useUIPermission()
const { allowCSVDownload } = useSharedView()
const isViewSidebarAvailable = computed(
() => (isGrid.value || isGallery.value || isKanban.value || isMap.value) && !isPublic.value,
)
</script>
<template>
<div
class="nc-table-toolbar h-12 min-h-12 py-1 flex gap-2 items-center px-3 border-b border-gray-200 overflow-hidden"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
class="nc-table-toolbar h-12 min-h-12 py-1 flex gap-2 items-center border-b border-gray-200 overflow-hidden"
:class="{
'nc-table-toolbar-mobile': isMobileMode,
'max-h-[var(--topbar-height)] min-h-[var(--topbar-height)]': !isMobileMode,
'pl-3 pr-0': isViewSidebarAvailable,
'px-3': !isViewSidebarAvailable,
}"
style="z-index: 7"
>
<template v-if="isViewsLoading">
@ -50,6 +59,7 @@ const { allowCSVDownload } = useSharedView()
v-if="(isGrid || isGallery || isKanban || isMap) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false"
/>
<LazySmartsheetToolbarOpenViewSidebarBtn v-if="isViewSidebarAvailable" />
</template>
</div>
</template>

11
packages/nc-gui/components/smartsheet/Topbar.vue

@ -22,13 +22,17 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<template>
<div
class="nc-table-topbar h-20 py-1 flex gap-2 items-center pr-2 pl-2.5 border-b border-gray-200 overflow-hidden relative"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
:class="{
'nc-table-toolbar-mobile': isMobileMode,
'max-h-[var(--topbar-height)] min-h-[var(--topbar-height)]': !isMobileMode,
}"
style="z-index: 7"
>
<template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" />
</template>
<template v-else>
<GeneralOpenLeftSidebarBtn />
<LazySmartsheetToolbarViewInfo v-if="!isPublic" />
<div v-if="!isMobileMode" class="flex-1" />
@ -38,6 +42,11 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<GeneralApiLoader />
<LazyGeneralShareProject v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic" is-view-toolbar />
<LazyGeneralLanguage
v-if="isSharedBase"
class="cursor-pointer text-lg hover:(text-black bg-gray-200) mr-0 p-1.5 rounded-md"
/>
</template>
</div>
</template>

13
packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue

@ -50,7 +50,7 @@ vModel.value.au = !!vModel.value.au */
<a-form-item label="NN">
<a-checkbox
v-model:checked="vModel.rqd"
:disabled="vModel.pk"
:disabled="vModel.pk || !sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-NN"
@change="onAlter"
/>
@ -59,6 +59,7 @@ vModel.value.au = !!vModel.value.au */
<a-form-item label="PK">
<a-checkbox
v-model:checked="vModel.pk"
:disabled="!sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-PK"
@change="onAlter"
/>
@ -67,17 +68,17 @@ vModel.value.au = !!vModel.value.au */
<a-form-item label="AI">
<a-checkbox
v-model:checked="vModel.ai"
:disabled="sqlUi.colPropUNDisabled(vModel)"
:disabled="sqlUi.colPropUNDisabled(vModel) || !sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-AI"
@change="onAlter"
/>
</a-form-item>
<a-form-item label="UN" :disabled="sqlUi.colPropUNDisabled(vModel)" @change="onAlter">
<a-form-item label="UN" :disabled="sqlUi.colPropUNDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter">
<a-checkbox v-model:checked="vModel.un" class="nc-column-checkbox-UN" />
</a-form-item>
<a-form-item label="AU" :disabled="sqlUi.colPropAuDisabled(vModel)" @change="onAlter">
<a-form-item label="AU" :disabled="sqlUi.colPropAuDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter">
<a-checkbox v-model:checked="vModel.au" class="nc-column-checkbox-AU" />
</a-form-item>
</div>
@ -95,13 +96,13 @@ vModel.value.au = !!vModel.value.au */
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input
v-model:value="vModel.dtxp"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt)"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)"
@input="onAlter"
/>
</a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input v-model:value="vModel.dtxs" @input="onAlter" />
<a-input v-model:value="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item>
</template>

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

@ -49,7 +49,7 @@ const { copy } = useCopy()
const copyRecordUrl = () => {
copy(
encodeURI(
`${dashboardUrl?.value}#/${route.params.typeOrId}/${route.params.projectId}/${route.params.type}/${meta.value?.id}${
`${dashboardUrl?.value}#/${route.params.typeOrId}/${route.params.projectId}/${meta.value?.id}${
props.view ? `/${props.view.title}` : ''
}?rowId=${primaryKey.value}`,
),
@ -127,12 +127,18 @@ const onConfirmDeleteRowClick = async () => {
/>
</a-tooltip>
<a-button class="nc-expand-form-save-btn !rounded-md" type="primary" :disabled="!isUIAllowed('tableRowUpdate')" @click="save">
<NcButton
class="nc-expand-form-save-btn !w-[60px]"
type="primary"
size="small"
:disabled="!isUIAllowed('tableRowUpdate')"
@click="save"
>
{{ $t('general.save') }}
</a-button>
</NcButton>
<a-dropdown>
<component :is="iconMap.threeDotVertical" class="nc-icon-transition nc-expand-form-more-actions" />
<component :is="iconMap.threeDotVertical" class="nc-icon-transition nc-expand-form-more-actions hover:cursor-pointer" />
<template #overlay>
<a-menu>
<a-menu-item v-if="!isNew" @click="loadRow()">
@ -174,9 +180,17 @@ const onConfirmDeleteRowClick = async () => {
</a-menu>
</template>
</a-dropdown>
<a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick">
<p>Are you sure you want to delete this row?</p>
</a-modal>
<GeneralModal v-model:visible="showDeleteRowModal" class="!w-[25rem]">
<div class="p-4">
<div class="prose-xl font-bold self-center">Delete row ?</div>
<div class="mt-4">Are you sure you want to delete this row?</div>
</div>
<div class="flex flex-row gap-x-2 mt-1 pt-1.5 justify-end p-4">
<NcButton type="secondary" @click="showDeleteRowModal = false">{{ $t('general.cancel') }}</NcButton>
<NcButton @click="onConfirmDeleteRowClick">{{ $t('general.confirm') }} </NcButton>
</div>
</GeneralModal>
</div>
</template>

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

@ -1124,7 +1124,7 @@ defineExpose({
})
// when expand is clicked the drawer should open
// and cell should loose focus
// and cell should loose focs
const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
if (expandForm) {
expandForm(row, col)
@ -1218,7 +1218,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
>
<div class="h-full w-[60px] flex items-center justify-center">
<GeneralIcon v-if="isEeUI && (altModifier || persistMenu)" icon="magic" class="text-sm text-orange-400" />
<component :is="iconMap.plus" class="text-sm nc-column-add text-gray-500 !group-hover:text-black" />
<component :is="iconMap.plus" class="text-base nc-column-add text-gray-500 !group-hover:text-black" />
</div>
<template v-if="isEeUI && persistMenu" #overlay>
@ -1469,7 +1469,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
>
<td class="text-left pointer sticky left-0 !border-r-0">
<div class="px-2 w-full flex items-center text-gray-500">
<component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-gray-600 group-hover:text-black" />
<component :is="iconMap.plus" class="text-pint-500 text-base ml-2 text-gray-600 group-hover:text-black" />
</div>
</td>
<td class="!border-gray-100" :colspan="visibleColLength"></td>
@ -1590,7 +1590,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
:extra-style="paginationStyleRef?.extraStyle"
>
<template #add-record>
<div v-if="isAddingEmptyRowAllowed" class="flex ml-2">
<div v-if="isAddingEmptyRowAllowed" class="flex ml-1">
<a-dropdown-button
class="nc-grid-add-new-row"
placement="top"

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { ColumnInj, IsFormInj, IsKanbanInj, IsLockedInj, inject, provide, ref, toRef, useUIPermission } from '#imports'
import { ColumnInj, IsFormInj, IsKanbanInj, inject, provide, ref, toRef, useUIPermission } from '#imports'
interface Props {
column: ColumnType
@ -18,8 +18,6 @@ const isDropDownOpen = ref(false)
const isKanban = inject(IsKanbanInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const column = toRef(props, 'column')
const { isUIAllowed } = useUIPermission()
@ -41,7 +39,7 @@ const closeAddColumnDropdown = () => {
}
const openHeaderMenu = () => {
if (!isLocked.value && !isForm.value && isUIAllowed('edit-column')) {
if (!isForm.value && isUIAllowed('edit-column')) {
editColumnDropdown.value = true
}
}
@ -59,7 +57,7 @@ const openHeaderMenu = () => {
<div
v-if="column"
class="name pl-1 !truncate"
:class="{ 'cursor-pointer pt-0.25': !isForm && isUIAllowed('edit-column') && !hideMenu && !isLocked }"
:class="{ 'cursor-pointer pt-0.25': !isForm && isUIAllowed('edit-column') && !hideMenu }"
style="white-space: pre-line"
:title="column.title"
>

2
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -303,7 +303,7 @@ const setIcon = async (icon: string, view: ViewType) => {
<a-menu
ref="menuRef"
:class="{ dragging }"
class="nc-views-menu flex flex-col !px-3 w-full !border-r-0 !bg-inherit nc-scrollbar-md"
class="nc-views-menu flex flex-col !ml-3 !mr-2.5 w-full !border-r-0 !bg-inherit nc-scrollbar-md"
:selected-keys="selected"
>
<!-- Lazy load breaks menu item active styles, i.e. styles never change even when active item changes -->

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

@ -37,6 +37,8 @@ const activeView = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false))
const { rightSidebarState } = storeToRefs(useSidebarStore())
const isDropdownOpen = ref(false)
const isEditing = ref(false)
@ -167,6 +169,12 @@ function onStopEdit() {
isStopped.value = false
}, 250)
}
watch(rightSidebarState, () => {
if (rightSidebarState.value === 'peekCloseEnd') {
isDropdownOpen.value = false
}
})
</script>
<template>

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

@ -16,9 +16,6 @@ import {
useViewsStore,
watch,
} from '#imports'
import FieldIcon from '~icons/nc-icons/eye'
const openedTab = ref<'views' | 'developer'>('views')
const { refreshCommandPalette } = useCommandPalette()
@ -50,12 +47,10 @@ const route = useRoute()
const { $e } = useNuxtApp()
const { rightSidebarSize } = storeToRefs(useSidebarStore())
const { isRightSidebarOpen } = storeToRefs(useSidebarStore())
const tabBtnsContainerRef = ref<HTMLElement | null>(null)
const minimalMode = ref(false)
/** Watch route param and change active view based on `viewTitle` */
watch(
[views, () => route.params.viewTitle],
@ -142,40 +137,6 @@ function onOpenModal({
close(1000)
}
}
const onTabChange = (tab: 'views' | 'developer') => {
openedTab.value = tab
}
const onResize = () => {
if (!tabBtnsContainerRef?.value) return
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize)
if (!tabBtnsContainerRef?.value?.offsetWidth) return
if (tabBtnsContainerRef?.value?.offsetWidth < 13 * remToPx) {
minimalMode.value = true
} else {
minimalMode.value = false
}
}
watch(
() => rightSidebarSize.value?.current,
() => {
onResize()
},
)
onMounted(() => {
window.addEventListener('resize', onResize)
onResize()
})
onUnmounted(() => {
window.removeEventListener('resize', onResize)
})
</script>
<template>
@ -186,9 +147,41 @@ onUnmounted(() => {
<div
v-else
ref="tabBtnsContainerRef"
class="flex flex-row p-1 mx-3 mt-2.25 mb-2.75 rounded-md gap-x-2 nc-view-sidebar-tab text-gray-500"
class="flex flex-row group py-1 mx-3.25 mt-1.25 mb-2.75 rounded-md gap-x-2 nc-view-sidebar-tab items-center justify-between"
>
Views
<div class="flex text-gray-600 ml-1.75">Views</div>
<NcTooltip
placement="bottomLeft"
hide-on-click
class="flex opacity-0 group-hover:(opacity-100) transition-all duration-50"
:class="{
'!w-8 !opacity-100': !isRightSidebarOpen,
}"
>
<template #title>
{{
isRightSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
</template>
<NcButton
type="text"
size="small"
class="nc-sidebar-left-toggle-icon !text-gray-600 !hover:text-gray-800"
@click="isRightSidebarOpen = !isRightSidebarOpen"
>
<div class="flex items-center text-inherit">
<GeneralIcon
icon="doubleRightArrow"
class="duration-150 transition-all"
:class="{
'transform rotate-180': !isRightSidebarOpen,
}"
/>
</div>
</NcButton>
</NcTooltip>
</div>
<div class="flex-1 flex flex-col min-h-0">

2
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -60,7 +60,7 @@ useMenuCloseOnEsc(open)
</script>
<template>
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu">
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu nc-toolbar-dropdown">
<div :class="{ 'nc-active-btn': filtersLength }">
<a-button v-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked">
<div class="flex items-center gap-2">

4
packages/nc-gui/components/smartsheet/toolbar/Export.vue

@ -6,8 +6,8 @@ import { iconMap } from '#imports'
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<component :is="iconMap.download" class="group-hover:text-accent text-gray-600" />
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('general.download') }}</span>
<component :is="iconMap.download" class="group-hover:text-accent text-gray-500" />
<span class="text-capitalize !text-sm font-medium text-gray-500">{{ $t('general.download') }}</span>
<component :is="iconMap.arrowDown" class="text-grey" />
</div>
</a-button>

2
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -278,7 +278,7 @@ useMenuCloseOnEsc(open)
</script>
<template>
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu">
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu nc-toolbar-dropdown">
<div :class="{ 'nc-active-btn': numberOfHiddenFields }">
<a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">

8
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -142,7 +142,13 @@ watch(open, () => {
</script>
<template>
<NcDropdown v-model:visible="open" offset-y class="" :trigger="['click']" overlay-class-name="nc-dropdown-group-by-menu">
<NcDropdown
v-model:visible="open"
offset-y
class=""
:trigger="['click']"
overlay-class-name="nc-dropdown-group-by-menu nc-toolbar-dropdown"
>
<div :class="{ 'nc-badge nc-active-btn': groupedByColumnIds?.length }">
<a-button v-e="['c:group-by']" class="nc-group-by-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">

53
packages/nc-gui/components/smartsheet/toolbar/OpenViewSidebarBtn.vue

@ -0,0 +1,53 @@
<script lang="ts" setup>
const { isRightSidebarOpen: _isRightSidebarOpen } = storeToRefs(useSidebarStore())
const isRightSidebarOpen = ref(_isRightSidebarOpen.value)
watch(_isRightSidebarOpen, (val) => {
if (val) {
isRightSidebarOpen.value = true
} else {
setTimeout(() => {
isRightSidebarOpen.value = false
}, 300)
}
})
const onClick = () => {
if (_isRightSidebarOpen.value) return
_isRightSidebarOpen.value = !_isRightSidebarOpen.value
}
</script>
<template>
<NcTooltip
placement="bottomLeft"
hide-on-click
class="transition-all duration-100"
:class="{
'!w-0 !opacity-0': isRightSidebarOpen,
'!w-8 !opacity-100 mr-2': !isRightSidebarOpen,
}"
>
<template #title>
{{
isRightSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
</template>
<NcButton
type="text"
size="small"
class="nc-sidebar-right-toggle-icon !text-gray-600 !hover:text-gray-800"
:class="{
'invisible !w-0': isRightSidebarOpen,
}"
@click="onClick"
>
<div class="flex items-center text-inherit">
<GeneralIcon icon="doubleLeftArrow" class="duration-150 transition-all !text-lg -mt-0.25" />
</div>
</NcButton>
</NcTooltip>
</template>

40
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -55,15 +55,19 @@ function onPressEnter() {
}
const displayColumnLabel = computed(() => {
if (search.value.field) {
// use search field label if specified
return columns.value?.find((column) => column.value === search.value.field)?.label
}
// use primary value label by default
return columns.value?.find((column) => column.primaryValue)?.label
})
watch(
() => (meta.value as TableType)?.columns,
() => search.value.field,
() => {
if (columns.value && search.value) search.value.field = columns.value.find((column) => column.primaryValue)?.value
onPressEnter()
},
{ immediate: true },
)
watchDebounced(
@ -81,29 +85,32 @@ watchDebounced(
<template>
<div
class="flex flex-row border-1 rounded-lg h-8 ml-1 border-gray-200 overflow-hidden"
:class="{ '!border-primary': search.query.length !== 0 || isFocused }"
:class="{ 'border-primary': search.query.length !== 0 || isFocused }"
>
<div
ref="searchDropdown"
class="flex items-center group relative px-2 cursor-pointer border-r-1 border-gray-200 hover:bg-gray-100"
:class="{ '!bg-gray-50 ': isDropdownOpen }"
:class="{ 'bg-gray-50 ': isDropdownOpen }"
@click="isDropdownOpen = !isDropdownOpen"
>
<GeneralIcon icon="search" class="ml-1 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<component :is="iconMap.arrowDown" class="ml-1 text-gray-400 !text-sm" />
<GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<div class="w-16 group-hover:w-12 text-[0.75rem] font-medium text-gray-400 truncate">
{{ displayColumnLabel }}
</div>
<div class="hidden group-hover:block">
<component :is="iconMap.arrowDown" class="text-gray-400 text-sm" />
</div>
<a-select
v-model:value="search.field"
:open="isDropdownOpen"
size="small"
:dropdown-match-select-width="false"
dropdown-class-name="!rounded-lg nc-dropdown-toolbar-search-field-option !w-48"
class="!py-1 !absolute top-0 left-0 w-full h-full z-10 !text-xs opacity-0"
dropdown-class-name="!rounded-lg nc-dropdown-toolbar-search-field-option w-48"
class="py-1 !absolute top-0 left-0 w-full h-full z-10 text-xs opacity-0"
>
<a-select-option v-for="op of columns" :key="op.value" :value="op.value">
<div class="flex items-center -ml-1 gap-2">
<SmartsheetHeaderIcon class="" :column="op.column" />
<div class="text-[0.75rem] flex items-center -ml-1 gap-2">
<SmartsheetHeaderIcon class="text-sm" :column="op.column" />
{{ op.label }}
</div>
</a-select-option>
@ -113,18 +120,17 @@ watchDebounced(
<a-input
v-model:value="search.query"
size="small"
class="!text-xs"
class="text-xs"
:style="{
width: '12rem',
width: '10rem',
}"
:placeholder="`${$t('general.search')} in ${displayColumnLabel}`"
:placeholder="$t('general.search')"
:bordered="false"
data-testid="search-data-input"
@press-enter="onPressEnter"
@focus="isFocused = true"
@blur="isFocused = false"
>
<template #addonBefore> </template>
</a-input>
</div>
</template>

9
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -108,7 +108,7 @@ watch(open, () => {
</script>
<template>
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-sort-menu">
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-sort-menu nc-toolbar-dropdown">
<div :class="{ 'nc-badge nc-active-btn': sorts?.length }">
<a-button v-e="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">
@ -167,7 +167,12 @@ watch(open, () => {
</template>
</div>
<NcDropdown v-if="availableColumns.length" v-model:visible="showCreateSort" :trigger="['click']">
<NcDropdown
v-if="availableColumns.length"
v-model:visible="showCreateSort"
:trigger="['click']"
overlay-class-name="nc-toolbar-dropdown"
>
<NcButton class="!text-brand-500" type="text" size="small" @click.stop="showCreateSort = true">
<div class="flex gap-1 items-center">
<component :is="iconMap.plus" />

32
packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue

@ -3,22 +3,24 @@ import { ViewTypes } from 'nocodb-sdk'
import { ActiveViewInj, inject } from '#imports'
const selectedView = inject(ActiveViewInj)
const { openedViewsTab } = storeToRefs(useViewsStore())
const { activeTable } = storeToRefs(useTablesStore())
</script>
<template>
<div
class="flex flex-row font-medium ml-1.5 items-center border-gray-50"
class="flex flex-row font-medium items-center border-gray-50 mt-0.5"
:class="{
'max-w-2/5': selectedView?.type !== ViewTypes.KANBAN,
'max-w-1/4': selectedView?.type === ViewTypes.KANBAN,
'min-w-2/5 max-w-2/5': selectedView?.type !== ViewTypes.KANBAN,
'min-w-1/4 max-w-1/4': selectedView?.type === ViewTypes.KANBAN,
}"
>
<div v-if="activeTable?.meta?.icon" class="text-lg mr-0.5">
{{ activeTable?.meta?.icon }}
</div>
<MdiTable v-else class="min-w-5 !text-gray-500 mb-0.25" :class="{}" />
<LazyGeneralEmojiPicker :emoji="activeTable?.meta?.icon" readonly size="xsmall">
<template #default>
<MdiTable class="min-w-5 !text-gray-500" :class="{}" />
</template>
</LazyGeneralEmojiPicker>
<span
class="text-ellipsis overflow-hidden pl-1 text-gray-500 max-w-1/2"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
@ -27,17 +29,15 @@ const { activeTable } = storeToRefs(useTablesStore())
</span>
<div class="px-2 text-gray-500">/</div>
<div v-if="selectedView?.meta?.icon" class="text-lg mr-0.5">
{{ selectedView?.meta?.icon }}
</div>
<GeneralViewIcon v-else :meta="{ type: selectedView?.type }" class="min-w-5 flex" />
<LazyGeneralEmojiPicker :emoji="selectedView?.meta?.icon" readonly size="xsmall">
<template #default>
<GeneralViewIcon :meta="{ type: selectedView?.type }" class="min-w-4.5 text-lg flex" />
</template>
</LazyGeneralEmojiPicker>
<span
class="text-ellipsis overflow-hidden pl-1.25 text-gray-700 max-w-1/2"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline', fontSize: '0.9rem' }"
>
<span class="truncate pl-1.25 text-gray-700 max-w-28/100">
{{ selectedView?.title }}
</span>
<LazySmartsheetToolbarReload />
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view'" />
</div>
</template>

14
packages/nc-gui/components/smartsheet/topbar/SelectMode.vue

@ -1,9 +1,7 @@
<script lang="ts" setup>
import { ActiveViewInj, inject, ref, storeToRefs, useViewsStore } from '#imports'
import { storeToRefs, useViewsStore } from '#imports'
const activeView = inject(ActiveViewInj, ref())
const { openedViewsTab } = storeToRefs(useViewsStore())
const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { onViewsTabChange } = useViewsStore()
</script>
@ -17,7 +15,8 @@ const { onViewsTabChange } = useViewsStore()
}"
@click="onViewsTabChange('view')"
>
<GeneralViewIcon class="tab-icon" :meta="{ type: activeView?.type }" ignore-color />
<GeneralViewIcon v-if="activeView?.type" class="tab-icon" :meta="{ type: activeView?.type }" ignore-color />
<GeneralLoader v-else class="tab-icon" />
<div class="tab-title nc-tab">Data</div>
</div>
<div
@ -32,7 +31,7 @@ const { onViewsTabChange } = useViewsStore()
class="tab-icon"
:class="{}"
:style="{
fontWeight: 600,
fontWeight: 500,
}"
/>
<div class="tab-title nc-tab">Details</div>
@ -46,7 +45,8 @@ const { onViewsTabChange } = useViewsStore()
}
.tab-icon {
font-size: 1.1rem;
font-size: 1.1rem !important;
@apply min-w-4.5;
}
.tab .tab-title {
@apply min-w-0;

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

@ -163,7 +163,7 @@ const onDrop = async (event: DragEvent) => {
<template>
<div class="nc-container flex flex-col h-full" @drop="onDrop" @dragover.prevent>
<LazySmartsheetTopbar />
<TabsSmartsheetResizable>
<TabsSmartsheetResizable style="height: calc(100% - var(--topbar-height))">
<template #content>
<div v-if="openedViewsTab === 'view'" class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar v-if="!isForm" />

211
packages/nc-gui/components/tabs/SmartsheetResizable.vue

@ -11,14 +11,11 @@ const {
const wrapperRef = ref<HTMLDivElement>()
const splitpaneWrapperRef = ref()
const { rightSidebarState: sidebarState } = storeToRefs(useSidebarStore())
const contentSize = computed(() => 100 - sideBarSize.value.current)
const isSidebarShort = ref(!isRightSidebarOpen.value)
const animationDuration = 300
const animationDuration = 250
const contentDomWidth = ref(window.innerWidth)
const isMouseOverShowSidebarZone = ref(false)
const isAnimationEndAfterSidebarHide = ref(!isRightSidebarOpen.value)
const isStartHideSidebarAnimation = ref(false)
const isLeftSidebarAnimating = ref(false)
const sidebarWidth = computed(() => (sideBarSize.value.old * contentDomWidth.value) / 100)
const currentSidebarSize = computed({
@ -29,58 +26,44 @@ const currentSidebarSize = computed({
},
})
const isSidebarHidden = ref(!isRightSidebarOpen.value)
watch(isRightSidebarOpen, () => {
sideBarSize.value.current = sideBarSize.value.old
if (isRightSidebarOpen.value) {
setTimeout(() => {
isSidebarShort.value = true
isSidebarHidden.value = false
}, 0)
setTimeout(() => (sidebarState.value = 'openStart'), 0)
setTimeout(() => {
isSidebarShort.value = false
}, animationDuration / 2)
setTimeout(() => (sidebarState.value = 'openEnd'), animationDuration)
} else {
sideBarSize.value.old = sideBarSize.value.current
isSidebarShort.value = true
isAnimationEndAfterSidebarHide.value = false
sidebarState.value = 'hiddenStart'
setTimeout(() => {
sideBarSize.value.current = 0
isSidebarHidden.value = true
isAnimationEndAfterSidebarHide.value = true
}, animationDuration * 1.75)
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
})
function handleMouseMove(e: MouseEvent) {
if (!wrapperRef.value) return
if (isRightSidebarOpen.value && !isSidebarHidden.value && !isMouseOverShowSidebarZone.value) return
if (isRightSidebarOpen.value) {
isSidebarHidden.value = false
isMouseOverShowSidebarZone.value = false
return
}
if (sidebarState.value === 'openEnd') return
const viewportWidth = window.innerWidth
if (e.clientX > viewportWidth - 14) {
isSidebarHidden.value = false
isMouseOverShowSidebarZone.value = true
} else if (e.clientX < viewportWidth - (sidebarWidth.value + 10) && !isSidebarHidden.value) {
isSidebarHidden.value = true
isMouseOverShowSidebarZone.value = false
isAnimationEndAfterSidebarHide.value = false
if (e.clientX > viewportWidth - 14 && ['hiddenEnd', 'peekCloseEnd'].includes(sidebarState.value)) {
sidebarState.value = 'peekOpenStart'
setTimeout(() => {
sidebarState.value = 'peekOpenEnd'
}, animationDuration)
} else if (e.clientX < viewportWidth - (sidebarWidth.value + 10) && sidebarState.value === 'peekOpenEnd') {
sidebarState.value = 'peekCloseOpen'
setTimeout(() => {
isAnimationEndAfterSidebarHide.value = true
}, animationDuration * 1.75)
sidebarState.value = 'peekCloseEnd'
}, animationDuration)
}
}
@ -89,7 +72,6 @@ function onWindowResize() {
}
onMounted(() => {
contentDomWidth.value = ((100 - leftSidebarWidthPercent.value) / 100) * window.innerWidth
document.addEventListener('mousemove', handleMouseMove)
window.addEventListener('resize', onWindowResize)
@ -100,76 +82,44 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize)
})
watch(leftSidebarWidthPercent, () => {
contentDomWidth.value = ((100 - leftSidebarWidthPercent.value) / 100) * window.innerWidth
})
watch(isLeftSidebarOpen, () => {
if (isLeftSidebarOpen.value) {
contentDomWidth.value = ((100 - leftSidebarWidthPercent.value) / 100) * window.innerWidth
} else {
contentDomWidth.value = window.innerWidth
}
isLeftSidebarAnimating.value = true
setTimeout(() => {
isLeftSidebarAnimating.value = false
}, 700)
})
watch(
() => !isRightSidebarOpen.value && isSidebarShort.value,
(value) => {
if (value) {
setTimeout(() => {
isStartHideSidebarAnimation.value = true
}, animationDuration / 2)
[isLeftSidebarOpen, leftSidebarWidthPercent],
() => {
if (isLeftSidebarOpen.value) {
contentDomWidth.value = ((100 - leftSidebarWidthPercent.value) / 100) * window.innerWidth
} else {
isStartHideSidebarAnimation.value = false
contentDomWidth.value = window.innerWidth
}
},
{
immediate: true,
},
)
</script>
<template>
<Splitpanes
ref="splitpaneWrapperRef"
style="height: calc(100vh - var(--topbar-height))"
class="smartsheet-resizable-wrapper w-full"
class="nc-view-sidebar-content-resizable-wrapper w-full h-full"
:class="{
'smartsheet-sidebar-short': isSidebarShort,
'hide-resize-bar': !isRightSidebarOpen || sidebarState === 'openStart',
}"
@resize="currentSidebarSize = $event[1].size"
>
<Pane :size="contentSize">
<slot name="content" />
</Pane>
<Pane
min-size="15%"
:size="currentSidebarSize"
max-size="40%"
class="nc-smartsheet-sidebar-splitpane !bg-transparent relative !overflow-visible !-ml-0.5"
>
<Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-view-sidebar-splitpane relative !overflow-visible">
<div
ref="wrapperRef"
class="nc-smartsheet-sidebar-wrapper relative z-10 !bg-transparent"
class="nc-view-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
:class="{
'open': isRightSidebarOpen,
'close': !isRightSidebarOpen,
'absolute': isMouseOverShowSidebarZone,
'smartsheet-sidebar-short': isSidebarShort,
'hide-sidebar': isStartHideSidebarAnimation && !isMouseOverShowSidebarZone,
'mouseover-show-sidebar-zone':
isSidebarShort && !isRightSidebarOpen && isMouseOverShowSidebarZone && isAnimationEndAfterSidebarHide,
'minimized-height': !isRightSidebarOpen,
'peek-sidebar': ['peekOpenEnd', 'peekCloseOpen'].includes(sidebarState),
'hide-sidebar': ['hiddenStart', 'hiddenEnd', 'peekCloseEnd'].includes(sidebarState),
}"
:style="{
width: isLeftSidebarAnimating
? '100%'
: isAnimationEndAfterSidebarHide && isSidebarHidden
? '0px'
: `${sidebarWidth}px`,
overflow: isMouseOverShowSidebarZone ? 'visible' : undefined,
translate: isMouseOverShowSidebarZone ? 'translateX(-100%)' : undefined,
width: sidebarState === 'hiddenEnd' ? '0px' : `${sidebarWidth}px`,
}"
>
<slot name="sidebar" />
@ -179,7 +129,35 @@ watch(
</template>
<style lang="scss">
.smartsheet-resizable-wrapper > {
.nc-view-sidebar-wrapper.minimized-height > * {
@apply pb-1 !(rounded-l-lg border-1 border-gray-200 shadow-lg);
height: 89.5%;
}
.nc-view-sidebar-wrapper > * {
transition: all 0.15s ease-in-out;
@apply z-10 absolute;
}
.nc-view-sidebar-wrapper.peek-sidebar {
> * {
@apply !opacity-100;
transform: translateX(-100%);
}
}
.nc-view-sidebar-wrapper.hide-sidebar {
@apply !min-w-0;
> * {
@apply opacity-0;
transform: translateX(100%);
}
}
/** Split pane CSS */
.nc-view-sidebar-content-resizable-wrapper > {
.splitpanes__splitter {
width: 0 !important;
position: relative;
@ -200,14 +178,14 @@ watch(
@apply bg-scrollbar;
z-index: 40;
width: 4px !important;
left: -4px;
left: -2px;
}
.splitpanes--dragging .splitpanes__splitter:before {
@apply bg-scrollbar;
z-index: 40;
width: 10px !important;
left: -6px;
left: -2px;
}
}
@ -215,62 +193,17 @@ watch(
@apply w-1 mr-0 bg-scrollbar;
z-index: 40;
width: 4px !important;
left: -4px;
left: -2px;
}
.splitpanes--dragging {
cursor: col-resize;
}
.smartsheet-sidebar-short > .splitpanes__splitter {
display: none !important;
background-color: transparent !important;
}
.nc-smartsheet-sidebar-wrapper {
@apply flex flex-col h-full justify-center min-w-36;
}
.nc-smartsheet-sidebar-wrapper.close {
> * {
height: 80vh;
}
}
.nc-smartsheet-sidebar-wrapper.smartsheet-sidebar-short {
> * {
height: 80h !important;
padding-bottom: 0.35rem;
margin-top: -3.25rem;
margin-left: 0.25rem;
}
}
.nc-smartsheet-sidebar-wrapper.open {
height: calc(100vh - var(--topbar-height));
> * {
height: calc(100vh - var(--topbar-height));
.nc-view-sidebar-content-resizable-wrapper.hide-resize-bar > {
.splitpanes__splitter {
display: none !important;
background-color: transparent !important;
}
}
.nc-smartsheet-sidebar-wrapper > * {
height: calc(100% - var(--topbar-height));
}
.nc-smartsheet-sidebar-wrapper > * {
width: 100%;
transition: all 0.3s ease-in-out;
}
.nc-smartsheet-sidebar-wrapper.hide-sidebar > * {
position: absolute;
opacity: 0;
transform: translateX(100%);
}
.mouseover-show-sidebar-zone > * {
transform: translateX(-100%);
}
.nc-smartsheet-sidebar-wrapper.smartsheet-sidebar-short > * {
@apply !(rounded-l-lg border-1 border-gray-200 shadow-lg);
}
</style>

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

@ -41,7 +41,7 @@ const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
<div class="w-full h-1 border-t-1 border-gray-100 opacity-50 mt-6"></div>
<div class="w-full flex flex-row justify-between items-baseline mt-6.5 mb-2 pr-0.25 ml-2">
<div class="text-xl">Collaborators</div>
<a-input v-model:value="userSearchText" class="!max-w-90 !rounded-md" placeholder="Search collaborators">
<a-input v-model:value="userSearchText" class="!max-w-90 !rounded-md mr-4" placeholder="Search collaborators">
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>
@ -109,7 +109,7 @@ const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
</NcBadge>
</a-select-option>
<template v-for="role of accessibleRoles" :key="`role-option-${role}`">
<a-select-option :value="role">
<a-select-option v-if="role" :value="role">
<NcBadge :color="RoleColors[role]">
<p class="badge-text">{{ RoleLabels[role] }}</p>
</NcBadge>
@ -232,4 +232,8 @@ tbody {
background: rgb(203, 203, 203);
}
}
:deep(.ant-select-selection-item) {
@apply mt-0.75;
}
</style>

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

@ -1,61 +1,42 @@
<script setup lang="ts">
import { NcProjectType, useRouter } from '#imports'
import type { NcButtonSize } from '~/lib'
const props = defineProps<{
activeWorkspaceId?: string | undefined
modal?: boolean
type?: string
isOpen: boolean
size?: NcButtonSize
centered?: boolean
}>()
const router = useRouter()
const { isUIAllowed } = useUIPermission()
const { orgRoles, workspaceRoles } = useRoles()
const projectStore = useProject()
const { isSharedBase } = storeToRefs(projectStore)
const workspaceStore = useWorkspace()
const { activeWorkspaceId: _activeWorkspaceId } = storeToRefs(workspaceStore)
const projectCreateDlg = ref(false)
const projectType = ref(NcProjectType.DB)
const navigateToCreateProject = (type: NcProjectType) => {
if (props.modal) {
projectType.value = type
projectCreateDlg.value = true
} else {
router.push({
path: '/create',
query: {
type,
workspaceId: props.activeWorkspaceId,
},
})
}
}
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (e.altKey && !e.shiftKey && !cmdOrCtrl) {
switch (e.keyCode) {
// ALT + D
case 68: {
e.stopPropagation()
navigateToCreateProject(NcProjectType.DB)
break
}
}
}
})
const size = computed(() => props.size || 'small')
const centered = computed(() => props.centered ?? true)
</script>
<template>
<div>
<a-button
class="!py-0 !px-0 !border-0 !h-full !rounded-md w-full hover:bg-gray-100 text-sm select-none cursor-pointer"
:type="props.type ?? 'primary'"
@click="navigateToCreateProject(NcProjectType.DB)"
>
<div class="flex w-full items-center gap-2">
<slot>{{ $t('title.newProj') }} <MdiMenuDown /></slot>
</div>
</a-button>
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" :type="projectType" />
</div>
<NcButton
v-if="isUIAllowed('projectCreate', false, workspaceRoles ?? orgRoles) && !isSharedBase"
type="text"
:size="size"
:centered="centered"
@click="projectCreateDlg = true"
>
<slot />
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" />
</NcButton>
</template>
<style scoped></style>

21
packages/nc-gui/components/workspace/CreateProjectDlg.vue

@ -64,22 +64,25 @@ watch(dialogShow, async (n, o) => {
if (n === o && !n) return
// Clear errors
form.value?.resetFields()
setTimeout(async () => {
form.value?.resetFields()
formState.value = {
title: 'Untitled Database',
}
await nextTick()
input.value?.$el?.focus()
input.value?.$el?.select()
formState.value = {
title: 'Untitled Database',
}
await nextTick()
input.value?.$el?.focus()
input.value?.$el?.select()
}, 5)
})
const typeLabel = computed(() => {
switch (props.type) {
case NcProjectType.DB:
return 'Database'
default:
return ''
return 'Database'
}
})
</script>

66
packages/nc-gui/components/workspace/Delete.vue

@ -1,66 +0,0 @@
<script lang="ts" setup>
const isConfirmed = ref(false)
const isDeleting = ref(false)
const { signOut } = useGlobal()
const { deleteWorkspace, navigateToWorkspace } = useWorkspace()
const { workspacesList, activeWorkspaceId } = storeToRefs(useWorkspace())
const onDelete = async () => {
isDeleting.value = true
try {
await deleteWorkspace(activeWorkspaceId.value)
isConfirmed.value = false
isDeleting.value = false
if (workspacesList.value.length > 0) {
await navigateToWorkspace(workspacesList.value[0].id)
} else {
await signOut()
setTimeout(() => {
window.location.href = '/'
}, 100)
}
} finally {
isDeleting.value = false
}
}
</script>
<template>
<div class="flex flex-col items-center">
<div class="item flex flex-col">
<div class="font-medium text-base">Delete Workspace</div>
<div class="text-gray-500 mt-2">Delete this workspace and all its contents.</div>
<div class="flex flex-row p-4 border-1 rounded-lg gap-x-2 mt-6">
<div class="flex">
<GeneralIcon icon="warning" class="text-xl text-orange-600" />
</div>
<div class="flex flex-col items-start gap-y-1">
<div class="flex font-medium">This action is irreversible.</div>
<div class="flex flex-row text-gray-500">
You have 31 days to undo this action by following the steps provided in recover workspace mail sent to your email.
</div>
</div>
</div>
<div class="flex flex-row mt-8 gap-x-2">
<a-checkbox v-model:checked="isConfirmed" />
<div class="flex">I understand that this action is irreversible</div>
</div>
<div class="flex flex-row w-full justify-end mt-8">
<NcButton type="danger" :disabled="!isConfirmed" :loading="isDeleting" @click="onDelete">
<template #loading> Deleting Workspace </template>
Delete Workspace
</NcButton>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.item {
@apply p-6 rounded-2xl border-1 max-w-180 mt-10;
}
</style>

21
packages/nc-gui/components/workspace/EmptyPlaceholder.vue

@ -7,25 +7,6 @@ const props = defineProps<{
const projectCreateDlg = ref(false)
const projectType = ref()
const { projects, projectsList } = storeToRefs(useProjects())
const router = useRouter()
const loading = ref(false)
// if at least one project exists, redirect to first project
watch(
projects,
(projects) => {
if (projects.size) {
return router.replace({
path: `/${router.currentRoute.value.params.typeOrId}/${projectsList.value[0].id}`,
})
}
loading.value = false
},
{ immediate: true },
)
const openCreateProjectDlg = (type: NcProjectType) => {
projectType.value = type
@ -35,7 +16,7 @@ const openCreateProjectDlg = (type: NcProjectType) => {
<template>
<div class="flex items-center justify-center h-full">
<div v-if="!loading" class="flex flex-col gap-4 items-center">
<div class="flex flex-col gap-4 items-center text-gray-500">
<NcIconsInbox />
<div class="font-weight-medium">No Projects</div>
<template v-if="props.buttons">

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

@ -46,7 +46,7 @@ const allowedRoles = computed<WorkspaceUserRoles[]>(() => {
<MdiChevronDown />
</template>
<template v-for="role of allowedRoles" :key="`role-option-${role}`">
<a-select-option :value="role">
<a-select-option v-if="role" :value="role">
<NcBadge :color="RoleColors[role]">
<p class="badge-text">{{ RoleLabels[role] }}</p>
</NcBadge>
@ -79,4 +79,8 @@ const allowedRoles = computed<WorkspaceUserRoles[]>(() => {
.badge-text {
@apply text-[14px] pt-1 text-center;
}
:deep(.ant-select-selection-item) {
@apply mt-0.75;
}
</style>

372
packages/nc-gui/components/workspace/Menu.vue

@ -1,371 +1,29 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import type { WorkspaceType } from 'nocodb-sdk'
import tinycolor from 'tinycolor2'
import { onMounted, projectThemeColors, ref, useWorkspace } from '#imports'
import { navigateTo } from '#app'
const props = defineProps<{
isOpen: boolean
}>()
const workspaceStore = useWorkspace()
const { saveTheme } = workspaceStore
const { isWorkspaceOwner } = storeToRefs(workspaceStore)
const { loadWorkspaces, clearWorkspaces } = workspaceStore
const { signOut, signedIn, user, token } = useGlobal()
const { copy } = useCopy(true)
const email = computed(() => user.value?.email ?? '---')
const { isUIAllowed } = useUIPermission()
const { theme, defaultTheme } = useTheme()
onMounted(async () => {
// await loadWorkspaces()
})
const workspaceModalVisible = ref(false)
const isWorkspaceDropdownOpen = ref(false)
const isAuthTokenCopied = ref(false)
const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => {
switch (mode) {
case 'swatch': {
if (color === defaultTheme.primaryColor) {
return await saveTheme(defaultTheme)
}
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
const complement = tcolor.complement()
await saveTheme({
primaryColor: color,
accentColor: complement.toHex8String(),
})
}
break
}
case 'primary': {
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
await saveTheme({
primaryColor: color,
})
}
break
}
case 'accent': {
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
await saveTheme({
accentColor: color,
})
}
break
}
}
}
const logout = async () => {
await signOut()
navigateTo('/signin')
}
const projectStore = useProject()
const { isSharedBase } = storeToRefs(projectStore)
// todo: temp
const modalVisible = false
const copyAuthToken = async () => {
try {
await copy(token.value!)
isAuthTokenCopied.value = true
} catch (e: any) {
console.error(e)
message.error(e.message)
}
}
onKeyStroke('Escape', () => {
if (isWorkspaceDropdownOpen.value) {
isWorkspaceDropdownOpen.value = false
}
})
</script>
<script lang="ts" setup></script>
<template>
<div class="flex-grow min-w-20">
<a-dropdown
v-model:visible="isWorkspaceDropdownOpen"
class="h-full min-w-0 flex-1"
:trigger="['click']"
placement="bottom"
overlay-class-name="nc-dropdown-workspace-menu"
>
<div class="flex flex-row flex-grow pl-0.5 pr-1 py-0.5 rounded-md w-full" style="max-width: calc(100% - 2.5rem)">
<div class="flex-grow min-w-20">
<div
:style="{ width: props.isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[props.isOpen ? '' : 'justify-center']"
data-testid="nc-workspace-menu"
class="group cursor-pointer flex gap-1 items-center nc-workspace-menu overflow-hidden py-1.25 pr-0.25"
class="flex items-center nc-workspace-menu overflow-hidden py-1.25 pr-0.25 justify-center w-full"
>
<slot name="brandIcon" />
<template v-if="props.isOpen">
Nocodb
<div class="flex flex-grow"></div>
<MdiCodeTags class="min-w-[17px] text-md transform rotate-90" />
</template>
<template v-else>
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" />
</template>
<a
class="w-10 min-w-10 transition-all duration-200 p-1 transform"
href="https://github.com/nocodb/nocodb"
target="_blank"
>
<img width="25" alt="NocoDB" src="~/assets/img/icons/256x256.png" />
</a>
<div class="font-semibold text-base">Nocodb</div>
<div class="flex flex-grow"></div>
</div>
<template #overlay>
<a-menu class="" @click="isWorkspaceDropdownOpen = false">
<a-menu-item-group class="!border-t-0">
<a-menu-divider />
<template v-if="!isSharedBase">
<!-- Copy Auth Token -->
<a-menu-item key="copy">
<div
v-e="['a:navbar:user:copy-auth-token']"
class="nc-workspace-menu-item group !gap-x-2"
@click.stop="copyAuthToken"
>
<GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black" />
<GeneralIcon v-else icon="copy" class="group-hover:text-black" />
<div v-if="isAuthTokenCopied">
{{ $t('activity.account.authTokenCopied') }}
</div>
<div v-else>
{{ $t('activity.account.authToken') }}
</div>
</div>
</a-menu-item>
<a-menu-divider v-if="false" />
<!-- Theme -->
<template v-if="isUIAllowed('projectTheme') && false">
<a-sub-menu key="theme">
<template #title>
<div class="nc-workspace-menu-item group">
<GeneralIcon icon="image" class="group-hover:text-accent" />
{{ $t('activity.account.themes') }}
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<LazyGeneralColorPicker
:model-value="theme.primaryColor"
:colors="projectThemeColors"
:row-size="9"
:advanced="false"
class="rounded-t"
@input="handleThemeColor('swatch', $event)"
/>
<!-- Custom Theme -->
<a-sub-menu key="theme-2">
<template #title>
<div class="nc-workspace-menu-item group">
{{ $t('labels.customTheme') }}
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<!-- Primary Color -->
<template #expandIcon></template>
<a-sub-menu key="pick-primary">
<template #title>
<div class="nc-workspace-menu-item group">
<ClarityColorPickerSolid class="group-hover:text-black" />
{{ $t('labels.primaryColor') }}
</div>
</template>
<template #expandIcon></template>
<LazyGeneralChromeWrapper @input="handleThemeColor('primary', $event)" />
</a-sub-menu>
<!-- Accent Color -->
<a-sub-menu key="pick-accent">
<template #title>
<div class="nc-workspace-menu-item group">
<ClarityColorPickerSolid class="group-hover:text-black" />
{{ $t('labels.accentColor') }}
</div>
</template>
<template #expandIcon></template>
<LazyGeneralChromeWrapper @input="handleThemeColor('accent', $event)" />
</a-sub-menu>
</a-sub-menu>
</a-sub-menu>
</template>
<a-menu-divider v-if="false" />
<!-- Preview As -->
<a-sub-menu v-if="isUIAllowed('previewAs') && false" key="preview-as">
<template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-workspace-menu-item group">
<GeneralIcon icon="preview" class="group-hover:text-black" />
{{ $t('activity.previewAs') }}
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<LazyGeneralPreviewAs />
</a-sub-menu>
</template>
<!-- Language -->
<a-sub-menu
v-if="!isEeUI"
key="language"
class="lang-menu !py-0"
popup-class-name="scrollbar-thin-dull min-w-50 max-h-90vh !overflow-auto"
>
<template #title>
<div class="nc-workspace-menu-item group">
<GeneralIcon icon="translate" class="group-hover:text-black nc-language mr-0.1" />
{{ $t('labels.language') }}
<div class="flex items-center text-gray-400 text-xs">(Community Translated)</div>
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<LazyGeneralLanguageMenu />
</a-sub-menu>
<!-- Account -->
<template v-if="signedIn && !isSharedBase">
<a-sub-menu key="account">
<template #title>
<div class="nc-workspace-menu-item group">
<GeneralIcon icon="account" class="group-hover:text-accent" />
{{ $t('labels.account') }}
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-e="['c:navbar:user:email']" class="nc-workspace-menu-item group !no-underline" to="/account/users">
<GeneralIcon icon="at" class="mt-1 group-hover:text-accent" />&nbsp;
<div class="prose-sm group-hover:text-primary">
<div>Account</div>
<div class="text-xs text-gray-500">{{ email }}</div>
</div>
</nuxt-link>
</a-menu-item>
<a-menu-item key="1" class="!rounded-b">
<div v-e="['a:navbar:user:sign-out']" class="nc-workspace-menu-item group" @click="logout">
<GeneralIcon icon="signout" class="group-hover:(!text-accent)" />&nbsp;
<span class="prose-sm nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-sub-menu>
</template>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
<GeneralModal v-model:visible="workspaceModalVisible" :class="{ active: modalVisible }" width="80%" :footer="null">
<div class="relative flex flex-col px-6 py-2">
<div class="absolute right-4 top-4 z-20">
<a-button type="text" class="!p-1 !h-7 !rounded" @click="workspaceModalVisible = false">
<component :is="iconMap.close" />
</a-button>
</div>
<a-tabs v-model:activeKey="tab">
<template v-if="isWorkspaceOwner">
<a-tab-pane key="collab" tab="Collaborators" class="w-full">
<WorkspaceCollaboratorsList class="h-full" />
</a-tab-pane>
<!-- <a-tab-pane key="settings" tab="Settings" class="w-full">
<div class="min-h-50 flex items-center justify-center">Not available</div>
</a-tab-pane> -->
</template>
</a-tabs>
</div>
</GeneralModal>
</div>
</div>
</template>
<style scoped lang="scss">
.nc-workspace-title-input {
@apply flex-grow py-2 px-3 outline-none hover:(bg-gray-50) focus:(bg-gray-50) font-medium rounded text-md text-defaault;
}
.nc-menu-sub-head {
@apply pt-2 pb-2 text-gray-500 text-sm px-5;
}
.nc-workspace-menu-item {
@apply flex items-center pl-2 py-2 gap-2 text-sm hover:text-black;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply hidden;
}
:deep(.ant-tabs-nav) {
@apply !mb-0;
}
:deep(.ant-dropdown-menu-submenu-title) {
@apply !py-0;
.nc-icon {
@apply !text-xs;
}
}
</style>

164
packages/nc-gui/components/workspace/Settings.vue

@ -0,0 +1,164 @@
<script lang="ts" setup>
const { signOut } = useGlobal()
const { deleteWorkspace, navigateToWorkspace, updateWorkspace } = useWorkspace()
const { workspacesList, activeWorkspaceId, activeWorkspace, workspaces } = storeToRefs(useWorkspace())
const formValidator = ref()
const isConfirmed = ref(false)
const isDeleting = ref(false)
const isErrored = ref(false)
const isTitleUpdating = ref(false)
const isCancelButtonVisible = ref(false)
const form = ref({
title: '',
})
const formRules = {
title: [
{ required: true, message: 'Workspace name required' },
{ min: 3, message: 'Workspace name must be at least 3 characters long' },
{ max: 50, message: 'Workspace name must be at most 50 characters long' },
],
}
const onDelete = async () => {
isDeleting.value = true
try {
await deleteWorkspace(activeWorkspaceId.value, { skipStateUpdate: true })
isConfirmed.value = false
isDeleting.value = false
// We only remove the delete workspace from the list after the api call is successful
workspaces.value.delete(activeWorkspaceId.value)
if (workspacesList.value.length > 1) {
await navigateToWorkspace(workspacesList.value[0].id)
} else {
// As signin page will clear the workspaces, we need to check if there are more than one workspace
await signOut(false)
setTimeout(() => {
window.location.href = '/'
}, 100)
}
} finally {
isDeleting.value = false
}
}
const titleChange = async () => {
const valid = await formValidator.value.validate()
if (!valid) return
if (isTitleUpdating.value) return
isTitleUpdating.value = true
isErrored.value = false
try {
await updateWorkspace(activeWorkspaceId.value, {
title: form.value.title,
})
} catch (e: any) {
console.error(e)
} finally {
isTitleUpdating.value = false
isCancelButtonVisible.value = false
}
}
watch(
() => activeWorkspace.value.title,
() => {
form.value.title = activeWorkspace.value.title
},
{
immediate: true,
},
)
watch(
() => form.value.title,
async () => {
try {
if (form.value.title !== activeWorkspace.value?.title) {
isCancelButtonVisible.value = true
} else {
isCancelButtonVisible.value = false
}
isErrored.value = !(await formValidator.value.validate())
} catch (e: any) {
isErrored.value = true
}
},
)
const onCancel = () => {
form.value.title = activeWorkspace.value?.title
}
</script>
<template>
<div class="flex flex-col items-center nc-workspace-settings-settings">
<div class="item flex flex-col w-full">
<div class="font-medium text-base">Change Workspace Name</div>
<a-form ref="formValidator" layout="vertical" no-style :model="form" class="w-full" @finish="titleChange">
<div class="text-gray-500 mt-6 mb-1.5">Workspace name</div>
<a-form-item name="title" :rules="formRules.title">
<a-input
v-model:value="form.title"
class="w-full !rounded-md !py-1.5"
placeholder="Workspace name"
data-testid="nc-workspace-settings-settings-rename-input"
/>
</a-form-item>
<div class="flex flex-row w-full justify-end mt-8 gap-4">
<NcButton
v-if="isCancelButtonVisible"
type="secondary"
html-type="submit"
data-testid="nc-workspace-settings-settings-rename-cancel"
@click="onCancel"
>
<template #loading> Renaming Workspace </template>
Cancel
</NcButton>
<NcButton
type="primary"
html-type="submit"
:disabled="isErrored || (form.title && form.title === activeWorkspace.title)"
:loading="isDeleting"
data-testid="nc-workspace-settings-settings-rename-submit"
>
<template #loading> Renaming Workspace </template>
Rename Workspace
</NcButton>
</div>
</a-form>
</div>
<div class="item flex flex-col">
<div class="font-medium text-base">Delete Workspace</div>
<div class="text-gray-500 mt-2">Delete this workspace and all its contents.</div>
<div class="flex flex-row mt-8 gap-x-2">
<a-checkbox v-model:checked="isConfirmed" />
<div class="flex">I understand that this action is irreversible</div>
</div>
<div class="flex flex-row w-full justify-end mt-8">
<NcButton type="danger" :disabled="!isConfirmed" :loading="isDeleting" @click="onDelete">
<template #loading> Deleting Workspace </template>
Delete Workspace
</NcButton>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.item {
@apply p-6 rounded-2xl border-1 max-w-180 mt-10 min-w-100 w-full;
}
</style>

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

@ -1,11 +1,14 @@
<script lang="ts" setup>
import { useTitle } from '@vueuse/core'
import type { WorkspaceType } from 'nocodb-sdk'
import { isEeUI } from '#imports'
const router = useRouter()
const route = router.currentRoute
const { isWorkspaceOwnerOrCreator, isWorkspaceOwner, activeWorkspace } = storeToRefs(useWorkspace())
const workspaceStore = useWorkspace()
const { isWorkspaceOwnerOrCreator, isWorkspaceOwner, activeWorkspace, workspaces } = storeToRefs(workspaceStore)
const { loadCollaborators } = workspaceStore
const { appInfo } = useGlobal()
@ -14,6 +17,7 @@ const tab = computed({
return route.value.query?.tab ?? 'collaborators'
},
set(tab: string) {
if (tab === 'collaborators') loadCollaborators()
router.push({ query: { ...route.value.query, tab } })
},
})
@ -33,15 +37,27 @@ watch(
immediate: true,
},
)
onMounted(() => {
until(() => activeWorkspace.value?.id)
.toMatch((v) => !!v)
.then(() => {
until(() => workspaces.value)
.toMatch((v) => v.has(activeWorkspace.value.id))
.then(() => {
loadCollaborators()
})
})
})
</script>
<template>
<div v-if="activeWorkspace" class="flex flex-col nc-workspace-container">
<div v-if="activeWorkspace" class="flex flex-col nc-workspace-settings">
<div class="flex gap-2 items-center min-w-0 p-6">
<span class="nc-workspace-avatar !w-8 !h-8" :style="{ backgroundColor: getWorkspaceColor(activeWorkspace) }">
{{ activeWorkspace?.title?.slice(0, 2) }}
</span>
<h1 class="text-3xl font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10">
<h1 class="text-3xl font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10 capitalize">
{{ activeWorkspace?.title }}
</h1>
</div>
@ -59,7 +75,7 @@ watch(
</a-tab-pane>
</template>
<template v-if="isWorkspaceOwner && appInfo.ee">
<template v-if="isWorkspaceOwner && isEeUI">
<a-tab-pane key="billing" class="w-full">
<template #tab>
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5">
@ -70,15 +86,15 @@ watch(
<WorkspaceBilling />
</a-tab-pane>
</template>
<template v-if="isWorkspaceOwner && appInfo.ee">
<template v-if="isWorkspaceOwner && isEeUI">
<a-tab-pane key="settings" class="w-full">
<template #tab>
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5">
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-settings">
<GeneralIcon icon="settings" />
Settings
</div>
</template>
<WorkspaceDelete />
<WorkspaceSettings />
</a-tab-pane>
</template>
</NcTabs>

46
packages/nc-gui/composables/useApi/interceptors.ts

@ -3,6 +3,8 @@ import { navigateTo, useGlobal, useRouter } from '#imports'
const DbNotFoundMsg = 'Database config not found'
let refreshTokenPromise: Promise<string> | null = null
export function addAxiosInterceptors(api: Api<any>) {
const state = useGlobal()
const router = useRouter()
@ -51,6 +53,40 @@ export function addAxiosInterceptors(api: Api<any>) {
return Promise.reject(error)
}
let refreshTokenPromiseRes: (token: string) => void
let refreshTokenPromiseRej: (e: Error) => void
// avoid multiple refresh token requests by multiple requests at the same time
// wait for the first request to finish and then retry the failed requests
if (refreshTokenPromise) {
// if previous refresh token request succeeds use the token and retry request
return refreshTokenPromise
.then((token) => {
// New request with new token
return new Promise((resolve, reject) => {
const config = error.config
config.headers['xc-auth'] = token
api.instance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
})
.catch(() => {
// ignore since it could have already been handled and redirected to sign in
})
} else {
// if
refreshTokenPromise = new Promise<string>((resolve, reject) => {
refreshTokenPromiseRes = resolve
refreshTokenPromiseRej = reject
})
}
// Try request again with new token
return api.instance
.post('/auth/token/refresh', null, {
@ -62,6 +98,10 @@ export function addAxiosInterceptors(api: Api<any>) {
config.headers['xc-auth'] = token.data.token
state.signIn(token.data.token)
// resolve the refresh token promise and reset
refreshTokenPromiseRes(token.data.token)
refreshTokenPromise = null
return new Promise((resolve, reject) => {
api.instance
.request(config)
@ -73,11 +113,15 @@ export function addAxiosInterceptors(api: Api<any>) {
})
})
})
.catch(async (error) => {
.catch(async (refreshTokenError) => {
await state.signOut()
if (!route.value.meta.public) navigateTo('/signIn')
// reject the refresh token promise and reset
refreshTokenPromiseRej(refreshTokenError)
refreshTokenPromise = null
return Promise.reject(error)
})
},

6
packages/nc-gui/composables/useCommandPalette/index.ts

@ -7,16 +7,16 @@ export const useCommandPalette = createSharedComposable(() => {
const cmdData = computed(() => {})
const activeScope = computed(() => {})
const activeScope = computed(() => ({} as any))
async function loadScope(_scope = 'root', _data?: any) {}
const loadTemporaryScope = (..._args: any) => {}
return {
commandPalette,
cmdData,
activeScope,
loadScope,
cmdPlaceholder,
refreshCommandPalette: refreshCommandPalette.trigger,
loadTemporaryScope,
}
})

3
packages/nc-gui/composables/useData.ts

@ -208,7 +208,6 @@ export function useData(args: {
},
undo: {
fn: async function undo(toUpdate: Row, property: string, pg: { page: number; pageSize: number }) {
console.log('undo', toUpdate, property, pg)
const updatedData = await updateRowProperty(
{ row: toUpdate.oldRow, oldRow: toUpdate.row, rowMeta: toUpdate.rowMeta },
property,
@ -218,12 +217,10 @@ export function useData(args: {
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) {
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]), formattedData.value)
if (rowIndex !== -1) {
console.log('manual')
const row = formattedData.value[rowIndex]
Object.assign(row.row, updatedData)
Object.assign(row.oldRow, updatedData)
} else {
console.log('fallback')
await callbacks?.loadData?.()
}
} else {

49
packages/nc-gui/composables/useGlobal/actions.ts

@ -9,7 +9,7 @@ export function useGlobalActions(state: State): Actions {
}
/** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = async () => {
const signOut: Actions['signOut'] = async (_skipRedirect = false) => {
try {
const nuxtApp = useNuxtApp()
await nuxtApp.$api.auth.signout()
@ -80,24 +80,59 @@ export function useGlobalActions(state: State): Actions {
workspaceId: _workspaceId,
type: _type,
projectId,
query,
}: {
workspaceId?: string
projectId?: string
type?: NcProjectType
query?: any
}) => {
const workspaceId = _workspaceId || 'nc'
let path: string
const queryParams = query ? `?${new URLSearchParams(query).toString()}` : ''
if (projectId) {
path = `/${workspaceId}/${projectId}`
path = `/${workspaceId}/${projectId}${queryParams}`
} else {
path = `/${workspaceId}`
path = `/${workspaceId}${queryParams}`
}
if (state.appInfo.value.baseHostName && location.hostname !== `${workspaceId}.${state.appInfo.value.baseHostName}`) {
location.href = `https://${workspaceId}.${state.appInfo.value.baseHostName}/dashboard/#${path}`
navigateTo({
path,
})
}
const ncNavigateTo = ({
workspaceId: _workspaceId,
type: _type,
projectId,
query,
tableId,
viewId,
}: {
workspaceId?: string
projectId?: string
type?: NcProjectType
query?: any
tableId?: string
viewId?: string
}) => {
const tablePath = tableId ? `/${tableId}${viewId ? `/${viewId}` : ''}` : ''
const workspaceId = _workspaceId || 'nc'
let path: string
const queryParams = query ? `?${new URLSearchParams(query).toString()}` : ''
if (projectId) {
path = `/${workspaceId}/${projectId}${tablePath}${queryParams}`
} else {
navigateTo(path)
path = `/${workspaceId}${queryParams}`
}
navigateTo({
path,
})
}
const getBaseUrl = (workspaceId: string) => {
@ -107,5 +142,5 @@ export function useGlobalActions(state: State): Actions {
return undefined
}
return { signIn, signOut, refreshToken, loadAppInfo, setIsMobileMode, navigateToProject, getBaseUrl }
return { signIn, signOut, refreshToken, loadAppInfo, setIsMobileMode, navigateToProject, getBaseUrl, ncNavigateTo }
}

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

@ -78,7 +78,9 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
/** current token ref, used by `useJwt` to reactively parse our token payload */
const token = computed({
get: () => storage.value.token || '',
set: (val) => (storage.value.token = val),
set: (val) => {
storage.value.token = val
},
})
const config = useRuntimeConfig()
@ -105,6 +107,7 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
isCloud: false,
automationLogLevel: 'OFF',
disableEmailAuth: false,
dashboardPath: '/dashboard',
})
/** reactive token payload */

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

@ -28,6 +28,7 @@ export interface AppInfo {
baseHostName?: string
disableEmailAuth: boolean
mainSubDomain?: string
dashboardPath: string
}
export interface StoredState {
@ -61,12 +62,20 @@ export interface Getters {
}
export interface Actions {
signOut: () => void
signOut: (skipRedirect?: boolean) => void
signIn: (token: string) => void
refreshToken: () => void
loadAppInfo: () => void
setIsMobileMode: (isMobileMode: boolean) => void
navigateToProject: (params: { workspaceId?: string; projectId?: string; type?: NcProjectType }) => void
navigateToProject: (params: { workspaceId?: string; projectId?: string; type?: NcProjectType; query?: any }) => void
ncNavigateTo: (params: {
workspaceId?: string
projectId?: string
type?: NcProjectType
query?: any
tableId?: string
viewId?: string
}) => void
getBaseUrl: (workspaceId: string) => string | undefined
}

7
packages/nc-gui/composables/useLTARStore.ts

@ -66,7 +66,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const { t } = useI18n()
const isPublic: boolean = inject(IsPublicInj, ref(false))
const isPublic: Ref<boolean> = inject(IsPublicInj, ref(false))
const colOptions = computed(() => column.value?.colOptions as LinkToAnotherRecordType)
@ -190,6 +190,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
where:
childrenListPagination.query && `(${relatedTableDisplayValueProp.value},like,${childrenListPagination.query})`,
} as any,
{
headers: {
'xc-password': sharedViewPassword.value,
},
},
)
} else {
childrenList.value = await $api.dbTableRow.nestedList(

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

@ -1,5 +1,5 @@
import { isString } from '@vue/shared'
import { computed, createSharedComposable, ref, useApi, useGlobal } from '#imports'
import { extractRolesObj } from 'nocodb-sdk'
import { computed, createSharedComposable, useApi, useGlobal } from '#imports'
import type { ProjectRole, Role, Roles } from '#imports'
/**
@ -9,40 +9,58 @@ import type { ProjectRole, Role, Roles } from '#imports'
* * `projectRoles` - the roles a user has in the current project (if one was loaded)
* * `allRoles` - all roles a user has (userRoles + projectRoles)
* * `hasRole` - a function to check if a user has a specific role
* * `loadProjectRoles` - a function to load the project roles for a specific project (by id)
* * `loadRoles` - a function to load reload user roles for scope
*/
export const useRoles = createSharedComposable(() => {
const { user, previewAs } = useGlobal()
const { api } = useApi()
const projectRoles = ref<Roles<ProjectRole>>({})
const allRoles = computed<Roles | null>(() => {
let orgRoles = user.value?.roles ?? {}
const userRoles = computed<Roles<Role>>(() => {
let roles = user.value?.roles ?? {}
orgRoles = extractRolesObj(orgRoles)
// if string populate key-value paired object
if (isString(roles)) {
roles = roles.split(',').reduce<Roles>((acc, role) => {
acc[role] = true
return acc
}, {})
let projectRoles = user.value?.project_roles ?? {}
projectRoles = extractRolesObj(projectRoles)
return {
...orgRoles,
...projectRoles,
}
})
const orgRoles = computed<Roles | null>(() => {
let orgRoles = user.value?.roles ?? {}
return roles
orgRoles = extractRolesObj(orgRoles)
return orgRoles
})
const allRoles = computed<Roles>(() => ({
...userRoles.value,
...projectRoles.value,
}))
const projectRoles = computed<Roles | null>(() => {
let projectRoles = user.value?.project_roles ?? {}
if (Object.keys(projectRoles).length === 0) {
projectRoles = user.value?.roles ?? {}
}
async function loadProjectRoles(
projectId: string,
projectRoles = extractRolesObj(projectRoles)
return projectRoles
})
const workspaceRoles = computed<Roles | null>(() => {
return null
})
async function loadRoles(
projectId?: string,
options: { isSharedBase?: boolean; sharedBaseId?: string; isSharedErd?: boolean; sharedErdId?: string } = {},
) {
if (options?.isSharedBase) {
const user = await api.auth.me(
const res = await api.auth.me(
{
project_id: projectId,
},
@ -53,9 +71,13 @@ export const useRoles = createSharedComposable(() => {
},
)
projectRoles.value = user.roles
user.value = {
...user.value,
roles: res.roles,
project_roles: res.project_roles,
}
} else if (options?.isSharedErd) {
const user = await api.auth.me(
const res = await api.auth.me(
{
project_id: projectId,
},
@ -66,12 +88,19 @@ export const useRoles = createSharedComposable(() => {
},
)
projectRoles.value = user.roles
user.value = {
...user.value,
roles: res.roles,
project_roles: res.project_roles,
}
} else if (projectId) {
const user = await api.auth.me({ project_id: projectId })
projectRoles.value = user.roles
} else {
projectRoles.value = {}
const res = await api.auth.me({ project_id: projectId })
user.value = {
...user.value,
roles: res.roles,
project_roles: res.project_roles,
}
}
}
@ -83,5 +112,5 @@ export const useRoles = createSharedComposable(() => {
return allRoles.value[role]
}
return { allRoles, userRoles, projectRoles, loadProjectRoles, hasRole }
return { allRoles, orgRoles, workspaceRoles, projectRoles, loadRoles, hasRole }
})

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

@ -34,6 +34,8 @@ export function useSharedView() {
const password = useState<string | undefined>('password', () => undefined)
provide(SharedViewPasswordInj, password)
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = useState<TableType | KanbanType | MapType | undefined>('meta', () => undefined)

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

@ -79,7 +79,7 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
}
await navigateTo({
path: `/${workspaceIdOrType}/${projectIdOrBaseId}/table/${table?.id}`,
path: `/${workspaceIdOrType}/${projectIdOrBaseId}/${table?.id}`,
query: route.value.query,
})

22
packages/nc-gui/composables/useUndoRedo.ts

@ -25,12 +25,22 @@ export const useUndoRedo = createSharedComposable(() => {
return tempScope
})
const isSameScope = (sc: { key: string; param: string }[]) => {
const isSameScope = (sc: { key: string; param: string | string[] }[]) => {
return sc.every((s) => {
return scope.value.some(
// viewTitle is optional for default view
(s2) =>
(s.key === 'viewTitle' && s2.key === 'viewTitle' && s2.param === '') || (s.key === s2.key && s.param === s2.param),
(s2) => {
if (Array.isArray(s.param)) {
return (
(s.key === 'viewTitle' && s2.key === 'viewTitle' && s2.param === '') ||
(s.key === s2.key && s.param.includes(s2.param))
)
} else {
return (
(s.key === 'viewTitle' && s2.key === 'viewTitle' && s2.param === '') || (s.key === s2.key && s.param === s2.param)
)
}
},
)
})
}
@ -130,18 +140,18 @@ export const useUndoRedo = createSharedComposable(() => {
}
}
const defineViewScope = (param: { view?: ViewType; project_id?: string; model_id?: string; title?: string }) => {
const defineViewScope = (param: { view?: ViewType; project_id?: string; model_id?: string; title?: string; id?: string }) => {
if (param.view) {
return [
{ key: 'projectId', param: param.view.project_id! },
{ key: 'viewId', param: param.view.fk_model_id! },
{ key: 'viewTitle', param: param.view.title! },
{ key: 'viewTitle', param: [param.view.title, param.view.id!] },
]
} else {
return [
{ key: 'projectId', param: param.project_id! },
{ key: 'viewId', param: param.model_id! },
{ key: 'viewTitle', param: param.title! },
{ key: 'viewTitle', param: [param.title!, param.id!] },
]
}
}

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { computed, iconMap, navigateTo, ref, useGlobal, useNuxtApp, useRoute, useSidebar } from '#imports'
const { signOut, signedIn, isLoading, user, currentVersion } = useGlobal()
const { signOut, signedIn, isLoading, user, currentVersion, appInfo } = useGlobal()
useSidebar('nc-left-sidebar', { hasSidebar: false })
@ -14,8 +14,8 @@ const hasSider = ref(false)
const sidebar = ref<HTMLDivElement>()
const logout = async () => {
await signOut()
await navigateTo('/signin')
await signOut(false)
navigateTo('/signin')
}
const { hooks } = useNuxtApp()
@ -68,7 +68,7 @@ hooks.hook('page:finish', () => {
<LazyGeneralReleaseInfo />
<a-tooltip placement="bottom" :mouse-enter-delay="1">
<a-tooltip v-if="!appInfo.ee" placement="bottom" :mouse-enter-delay="1">
<template #title> Switch language</template>
<div class="flex pr-4 items-center">
@ -128,7 +128,7 @@ hooks.hook('page:finish', () => {
</template>
</a-layout-header>
<a-tooltip placement="bottom">
<a-tooltip v-if="!appInfo.ee" placement="bottom">
<template #title> Switch language</template>
<LazyGeneralLanguage v-if="!signedIn && !route.params.projectId && !route.params.erdUuid" class="nc-lang-btn" />

208
packages/nc-gui/layouts/dashboard.vue

@ -5,22 +5,18 @@ import 'splitpanes/dist/splitpanes.css'
const router = useRouter()
const route = router.currentRoute
const { isLeftSidebarOpen, leftSidebarWidthPercent } = storeToRefs(useSidebarStore())
const {
isLeftSidebarOpen,
leftSidebarWidthPercent,
leftSideBarSize: sideBarSize,
leftSidebarState: sidebarState,
} = storeToRefs(useSidebarStore())
const wrapperRef = ref<HTMLDivElement>()
const sideBarSize = ref({
old: 17.5,
current: 17.5,
})
const contentSize = ref({
old: 82.5,
current: 82.5,
})
const isSidebarShort = ref(false)
const animationDuration = 300
const contentSize = computed(() => 100 - sideBarSize.value.current)
const animationDuration = 250
const viewportWidth = ref(window.innerWidth)
const isMouseOverShowSidebarZone = ref(false)
const isAnimationEndAfterSidebarHide = ref(false)
const isStartHideSidebarAnimation = ref(false)
const sidebarWidth = computed(() => (sideBarSize.value.old * viewportWidth.value) / 100)
const currentSidebarSize = computed({
@ -31,70 +27,46 @@ const currentSidebarSize = computed({
},
})
watch(
currentSidebarSize,
() => {
leftSidebarWidthPercent.value = currentSidebarSize.value
},
{
immediate: true,
},
)
const isSidebarHidden = ref(false)
watch(currentSidebarSize, () => {
leftSidebarWidthPercent.value = currentSidebarSize.value
})
watch(isLeftSidebarOpen, () => {
sideBarSize.value.current = sideBarSize.value.old
if (isLeftSidebarOpen.value) {
contentSize.value.current = contentSize.value.old
setTimeout(() => {
isSidebarShort.value = true
isSidebarHidden.value = false
}, 0)
setTimeout(() => (sidebarState.value = 'openStart'), 0)
setTimeout(() => {
isSidebarShort.value = false
}, animationDuration / 2)
setTimeout(() => (sidebarState.value = 'openEnd'), animationDuration)
} else {
sideBarSize.value.old = sideBarSize.value.current
contentSize.value.current = contentSize.value.old
contentSize.value.current = 100
isSidebarShort.value = true
isAnimationEndAfterSidebarHide.value = false
sidebarState.value = 'hiddenStart'
setTimeout(() => {
isSidebarHidden.value = true
sideBarSize.value.current = 0
isAnimationEndAfterSidebarHide.value = true
}, animationDuration * 1.75)
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
})
function handleMouseMove(e: MouseEvent) {
if (!wrapperRef.value) return
if (isLeftSidebarOpen.value && !isSidebarHidden.value && !isMouseOverShowSidebarZone.value) return
if (isLeftSidebarOpen.value) {
isSidebarHidden.value = false
isMouseOverShowSidebarZone.value = false
return
}
if (sidebarState.value === 'openEnd') return
if (e.clientX < 4) {
isSidebarHidden.value = false
isMouseOverShowSidebarZone.value = true
} else if (e.clientX > sidebarWidth.value + 10 && !isSidebarHidden.value) {
isSidebarHidden.value = true
isMouseOverShowSidebarZone.value = false
isAnimationEndAfterSidebarHide.value = false
if (e.clientX < 4 && ['hiddenEnd', 'peekCloseEnd'].includes(sidebarState.value)) {
sidebarState.value = 'peekOpenStart'
setTimeout(() => {
sidebarState.value = 'peekOpenEnd'
}, animationDuration)
} else if (e.clientX > sidebarWidth.value + 10 && sidebarState.value === 'peekOpenEnd') {
sidebarState.value = 'peekCloseOpen'
setTimeout(() => {
isAnimationEndAfterSidebarHide.value = true
}, animationDuration * 1.75)
sidebarState.value = 'peekCloseEnd'
}, animationDuration)
}
}
@ -113,19 +85,6 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize)
})
watch(
() => !isLeftSidebarOpen.value && isSidebarShort.value,
(value) => {
if (value) {
setTimeout(() => {
isStartHideSidebarAnimation.value = true
}, animationDuration / 2)
} else {
isStartHideSidebarAnimation.value = false
}
},
)
watch(route, () => {
if (route.value.name === 'index-index') {
isLeftSidebarOpen.value = true
@ -140,37 +99,32 @@ export default {
</script>
<template>
<NuxtLayout>
<NuxtLayout class="h-screen">
<slot v-if="!route.meta.hasSidebar" name="content" />
<Splitpanes
v-else
style="height: 100vh"
class="nc-sidebar-content-resizable-wrapper w-full"
class="nc-sidebar-content-resizable-wrapper w-full h-full"
:class="{
'sidebar-short': isSidebarShort,
'hide-resize-bar': !isLeftSidebarOpen || sidebarState === 'openStart',
}"
@resize="currentSidebarSize = $event[0].size"
>
<Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-sidebar-splitpane relative !overflow-visible">
<div
ref="wrapperRef"
class="nc-sidebar-wrapper relative"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
:class="{
'open': isLeftSidebarOpen,
'close': !isLeftSidebarOpen,
'absolute': isMouseOverShowSidebarZone,
'sidebar-short': isSidebarShort,
'hide-sidebar': isStartHideSidebarAnimation && !isMouseOverShowSidebarZone,
'minimized-height': !isLeftSidebarOpen,
'hide-sidebar': ['hiddenStart', 'hiddenEnd', 'peekCloseEnd'].includes(sidebarState),
}"
:style="{
width: isAnimationEndAfterSidebarHide && isSidebarHidden ? '0px' : `${sidebarWidth}px`,
overflow: isMouseOverShowSidebarZone ? 'visible' : undefined,
width: sidebarState === 'hiddenEnd' ? '0px' : `${sidebarWidth}px`,
}"
>
<slot name="sidebar" />
</div>
</Pane>
<Pane :size="contentSize.current">
<Pane :size="contentSize">
<slot name="content" />
</Pane>
</Splitpanes>
@ -178,20 +132,34 @@ export default {
</template>
<style lang="scss">
.nc-sidebar-wrapper.minimized-height > * {
@apply h-4/5 pb-2 !(rounded-r-lg border-1 border-gray-200 shadow-lg);
width: calc(100% + 4px);
}
.nc-sidebar-wrapper > * {
transition: all 0.2s ease-in-out;
@apply z-10 absolute;
}
.nc-sidebar-wrapper.hide-sidebar {
@apply !min-w-0;
> * {
@apply opacity-0;
transform: translateX(-100%);
}
}
/** Split pane CSS */
.nc-sidebar-content-resizable-wrapper > {
.splitpanes__splitter {
width: 0 !important;
position: relative;
overflow: visible;
@apply !w-0 relative overflow-visible;
}
.splitpanes__splitter:before {
@apply bg-gray-200 w-0.25;
@apply bg-gray-200 w-0.25 absolute left-0 top-0 h-full z-40;
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
z-index: 40;
}
.splitpanes__splitter:hover:before {
@ -209,57 +177,25 @@ export default {
.splitpanes--dragging .splitpanes__splitter {
@apply w-1 mr-0;
}
}
.sidebar-short > .splitpanes__splitter {
display: none !important;
.nc-sidebar-content-resizable-wrapper.hide-resize-bar > {
.splitpanes__splitter {
cursor: default !important;
opacity: 0 !important;
background-color: transparent !important;
}
}
.splitpanes--dragging {
cursor: col-resize;
}
.nc-sidebar-wrapper {
@apply flex flex-col h-full justify-center !min-w-32;
.splitpanes__pane {
transition: width 0.15s ease-in-out !important;
}
.nc-sidebar-wrapper.close {
> * {
height: 80vh;
}
}
.nc-sidebar-wrapper.sidebar-short {
> * {
@apply z-10;
height: 80vh !important;
padding-bottom: 0.35rem;
}
}
.splitpanes--dragging {
cursor: col-resize;
.nc-sidebar-wrapper.open {
height: 100vh;
> * {
height: 100vh;
> .splitpanes__pane {
transition: none !important;
}
}
.nc-sidebar-wrapper > * {
height: calc(100% - var(--sidebar-top-height));
}
.nc-sidebar-wrapper > * {
width: 100%;
transition: all 0.3s ease-in-out;
}
.nc-sidebar-wrapper.hide-sidebar > * {
position: absolute;
transform: translateX(-100%);
opacity: 0;
}
.nc-sidebar-wrapper.sidebar-short > * {
@apply !(rounded-r-lg border-1 border-gray-200 shadow-lg);
}
</style>

10
packages/nc-gui/layouts/empty.vue

@ -1,3 +1,11 @@
<script lang="ts">
export default {
name: 'EmptyLayout',
}
</script>
<template>
<slot></slot>
<NuxtLayout class="h-screen">
<slot></slot>
</NuxtLayout>
</template>

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

@ -34,7 +34,7 @@ watch(hasSidebar, (val) => {
})
const logout = async () => {
await signOut()
await signOut(false)
await navigateTo('/signin')
await clearWorkspaces()
}

32
packages/nc-gui/layouts/shared-view.vue

@ -76,40 +76,12 @@ export default {
</div>
</div>
<a-tooltip placement="bottom">
<template #title> Switch language</template>
<LazyGeneralLanguage class="nc-lang-btn" />
</a-tooltip>
<div class="flex-1" />
</a-layout-header>
<div class="w-full overflow-scroll" style="height: calc(100vh)">
<div class="w-full overflow-hidden" style="height: calc(100vh)">
<slot />
</div>
</a-layout>
</a-layout>
</template>
<style lang="scss">
.nc-lang-btn {
@apply color-transition flex items-center justify-center fixed bottom-10 right-10 z-99 w-12 h-12 rounded-full shadow-md shadow-gray-500 p-2 !bg-primary text-white ring-opacity-100 active:(ring ring-accent) hover:(ring ring-accent);
&::after {
@apply rounded-full absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-accent ring-opacity-100;
}
&:active::after {
@apply ring ring-accent ring-opacity-100;
}
}
.nc-navbar {
@apply flex !bg-white items-center !pl-2 !pr-5;
}
</style>

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

@ -20,6 +20,8 @@ interface User {
firstname: string | null
lastname: string | null
roles: Roles | string
project_roles: Roles | string
workspace_roles: Roles | string
invite_token?: string
project_id?: string
}
@ -138,7 +140,7 @@ type NcProject = ProjectType & {
interface UndoRedoAction {
undo: { fn: Function; args: any[] }
redo: { fn: Function; args: any[] }
scope?: { key: string; param: string }[]
scope?: { key: string; param: string | string[] }[]
}
interface ImportWorkerPayload {
@ -182,6 +184,8 @@ interface Users {
type ViewPageType = 'view' | 'webhook' | 'api' | 'field' | 'relation'
type NcButtonSize = 'xxsmall' | 'xsmall' | 'small' | 'medium'
export {
User,
ProjectMetaInfo,
@ -207,4 +211,5 @@ export {
AllRoles,
Users,
ViewPageType,
NcButtonSize,
}

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

@ -67,6 +67,11 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
/** if user is still not signed in, redirect to signin page */
if (!state.signedIn.value) return navigateTo('/signin')
} else if (to.meta.requiresAuth === false && state.signedIn.value) {
if (to.query?.logout) {
await state.signOut(true)
return navigateTo('/signin')
}
/**
* if user was turned away from non-auth page but also came from a non-auth page (e.g. user went to /signin and reloaded the page)
* redirect to home page

2
packages/nc-gui/package.json

@ -81,7 +81,7 @@
"vue-barcode-reader": "^1.0.3",
"vue-chartjs": "^5.2.0",
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
"vue-github-button": "^3.1.0",
"vue-i18n": "^9.2.2",
"vue-qrcode-reader": "3.1.0-vue3-compatibility.2",
"vue3-calendar-heatmap": "^2.0.0",

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

@ -5,12 +5,6 @@ const { isUIAllowed } = useUIPermission()
const $route = useRoute()
const { appInfo } = useGlobal()
const { loadScope } = useCommandPalette()
loadScope('account_settings')
const selectedKeys = computed(() => [
/^\/account\/users\/?$/.test($route.fullPath)
? isUIAllowed('superAdminUserManagement')

4
packages/nc-gui/pages/forgot-password.vue

@ -10,10 +10,6 @@ const { api, isLoading, error } = useApi({ useGlobalInstance: true })
const { t } = useI18n()
const { loadScope } = useCommandPalette()
loadScope('disabled')
const success = ref(false)
const formValidator = ref()

29
packages/nc-gui/pages/index.vue

@ -14,6 +14,10 @@ const projectId = ref<string>()
const projectsStore = useProjects()
const { populateWorkspace } = useWorkspace()
const { signedIn } = useGlobal()
const router = useRouter()
const route = router.currentRoute
@ -35,12 +39,6 @@ const isSharedView = computed(() => {
// check route is not project page by route name
return !routeName.startsWith('index-typeOrId-projectId-') && !['index', 'index-typeOrId'].includes(routeName)
})
const isSharedFormView = computed(() => {
const routeName = (route.value.name as string) || ''
// check route is shared form view route
return routeName.startsWith('index-typeOrId-form-viewId')
})
watch(
() => route.value.params.typeOrId,
@ -50,7 +48,19 @@ watch(
return
}
await projectsStore.loadProjects('recent')
// avoid loading projects for shared base
if (route.value.params.typeOrId === 'base') {
await populateWorkspace()
return
}
if (!signedIn.value) {
navigateTo('/signIn')
return
}
// Load projects
await populateWorkspace()
if (!route.value.params.projectId && projectsList.value.length > 0) {
await autoNavigateToProject()
@ -73,10 +83,7 @@ provide(ToggleDialogInj, toggleDialog)
<template>
<div>
<NuxtLayout v-if="isSharedFormView">
<NuxtPage />
</NuxtLayout>
<NuxtLayout v-else-if="isSharedView" name="shared-view">
<NuxtLayout v-if="isSharedView" name="shared-view">
<NuxtPage />
</NuxtLayout>
<NuxtLayout v-else name="dashboard">

2
packages/nc-gui/pages/index/[typeOrId].vue

@ -5,7 +5,7 @@ const route = router.currentRoute
</script>
<template>
<div class="h-full w-full">
<div>
<NuxtPage :transition="false" :page-key="route.params.typeOrId" />
</div>
</template>

52
packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index.vue

@ -18,58 +18,6 @@ const route = router.currentRoute
<template>
<div class="h-full w-full nc-container">
<div class="h-full w-full flex flex-col">
<!-- <div class="flex items-end !min-h-[var(--sidebar-top-height)] !bg-white-500 nc-tab-bar">
<div
v-if="!isOpen"
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row py-2 px-3 mb-1"
>
<GeneralIcon
v-e="['c:grid:toggle-navdraw']"
icon="sidebarMinimise"
class="cursor-pointer transform transition-transform duration-500 text-gray-500/80 hover:text-gray-500"
:class="{ 'rotate-180': !isOpen }"
@click="toggle(!isOpen)"
/>
</div>
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs min-w-[500px]" type="editable-card" @edit="onEdit">
<a-tab-pane v-for="(tab, i) of tabs" :key="i">
<template #tab>
<div class="flex items-center gap-2" data-testid="nc-tab-title">
<div class="flex items-center">
<Icon
v-if="tab.meta?.icon"
:icon="tab.meta?.icon"
class="text-xl"
:data-testid="`nc-tab-icon-${tab.meta?.icon}`"
/>
<component :is="icon(tab)" v-else class="text-sm" />
</div>
<div :data-testid="`nc-root-tabs-${tab.title}`">
<GeneralTruncateText :key="tab.title" :length="12">
{{ tab.title }}
</GeneralTruncateText>
</div>
</div>
</template>
</a-tab-pane>
</a-tabs>
<span class="flex-1" />
<div class="flex justify-center self-center mr-2 min-w-[115px]">
<div v-if="isLoading" class="flex items-center gap-2 ml-3 text-gray-200" data-testid="nc-loading">
{{ $t('general.loading') }}
<MdiLoading class="animate-infinite animate-spin" />
</div>
</div>
<LazyGeneralShareBaseButton class="mb-1px" />
<LazyGeneralFullScreen class="nc-fullscreen-icon mb-1px" />
</div>
-->
<div class="w-full min-h-[300px] flex-auto">
<NuxtPage />
</div>

6
packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index/[type]/[viewId]/[[viewTitle]].vue → packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index/[viewId]/[[viewTitle]].vue

@ -13,10 +13,6 @@ const activeTab = inject(
computed(() => ({} as TabItem)),
)
const viewType = computed(() => {
return route.params.type as string
})
watch(
() => route.params.viewId,
(viewId) => {
@ -33,6 +29,6 @@ watch(
<template>
<div class="w-full h-full relative">
<LazyTabsSmartsheet :key="viewType" :active-tab="activeTab" />
<LazyTabsSmartsheet :active-tab="activeTab" />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save