Browse Source

Merge branch 'develop' into NCDBOSS-77

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

2
.github/workflows/dispatch-oss.yml

@ -11,7 +11,7 @@ jobs:
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v6
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.OSS_DISPATCH }}
script: | script: |
const result = await github.rest.repos.createDispatchEvent({ const result = await github.rest.repos.createDispatchEvent({
owner: 'nocodb', owner: 'nocodb',

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'; @import '@vue-flow/core/dist/theme-default.css';
:root { :root {
--sidebar-top-height: 9.75rem;
--topbar-height: 3.1rem; --topbar-height: 3.1rem;
--sidebar-bottom-height: 8.5rem;
--new-header-height: 3.5rem; --new-header-height: 3.5rem;
--tw-text-opacity: 1; --tw-text-opacity: 1;
--navbar-bg: #FAFAFA; --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 { .nc-scrollbar-x-md {
overflow-x: scroll; overflow-x: scroll;
@ -505,7 +528,7 @@ a {
padding: 0 !important; padding: 0 !important;
} }
.ant-popover-inner-content { .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 { .ant-tooltip-inner {
@apply !px-1.5 !py-1 text-xs text-white bg-black; @apply !px-1.5 !py-1 text-xs text-white bg-black;
@ -515,4 +538,8 @@ a {
.ant-skeleton-input { .ant-skeleton-input {
@apply !h-full; @apply !h-full;
}
.nc-toolbar-dropdown {
@apply !rounded-2xl;
} }

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

@ -50,6 +50,7 @@ declare module '@vue/runtime-core' {
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup'] AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover']
ARadio: typeof import('ant-design-vue/es')['Radio'] ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate'] ARate: typeof import('ant-design-vue/es')['Rate']
@ -77,15 +78,12 @@ declare module '@vue/runtime-core' {
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default'] CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default'] CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClarityColorPickerSolid: typeof import('~icons/clarity/color-picker-solid')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default'] ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['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'] IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default'] IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default'] IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['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'] LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default'] MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default'] MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default']
@ -93,10 +91,7 @@ declare module '@vue/runtime-core' {
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default'] MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default'] MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default'] 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'] 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'] MaterialSymbolsKeyboardArrowDownRounded: typeof import('~icons/material-symbols/keyboard-arrow-down-rounded')['default']
MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default'] MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default']
MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default'] MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default']
@ -125,23 +120,16 @@ declare module '@vue/runtime-core' {
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default'] MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default'] MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['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'] MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiCircleMedium: typeof import('~icons/mdi/circle-medium')['default'] MdiCircleMedium: typeof import('~icons/mdi/circle-medium')['default']
MdiClose: typeof import('~icons/mdi/close')['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'] MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['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'] MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['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'] MdiEye: typeof import('~icons/mdi/eye')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default'] MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default'] MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default'] MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLinkVariant: typeof import('~icons/mdi/link-variant')['default'] MdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
@ -152,8 +140,6 @@ declare module '@vue/runtime-core' {
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default'] MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default'] MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiPlus: typeof import('~icons/mdi/plus')['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'] MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default'] MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-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"> <table class="w-full nc-webhooks-params">
<thead class="h-8"> <thead class="h-8">
<tr> <tr>
<th></th>
<th> <th>
<div class="text-left font-normal ml-2">Parameter Name</div> <div class="text-left font-normal ml-2">Parameter Name</div>
</th> </th>
@ -40,11 +39,6 @@ const deleteParamRow = (i: number) => {
<tbody> <tbody>
<tr v-for="(paramRow, idx) in vModel" :key="idx" class="!h-2 overflow-hidden"> <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"> <td class="px-2">
<a-form-item class="form-item"> <a-form-item class="form-item">
<a-input v-model:value="paramRow.name" placeholder="Key" class="!rounded-lg" /> <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"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk'
import { import {
ActiveCellInj, ActiveCellInj,
CellClickHookInj, CellClickHookInj,
@ -236,11 +237,16 @@ const clickHandler = () => {
} }
cellClickHandler() cellClickHandler()
} }
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
</script> </script>
<template> <template>
<a-date-picker <a-date-picker
v-model:value="localState" v-model:value="localState"
:disabled="isColDisabled"
:show-time="true" :show-time="true"
:bordered="false" :bordered="false"
class="!w-full !px-0 !border-none" class="!w-full !px-0 !border-none"
@ -251,7 +257,6 @@ const clickHandler = () => {
:input-read-only="true" :input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`" :dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`"
:open="readOnly || (localState && isPk) || isLockedMode ? false : open && (active || editable)" :open="readOnly || (localState && isPk) || isLockedMode ? false : open && (active || editable)"
:disabled="readOnly || (localState && isPk)"
@click="clickHandler" @click="clickHandler"
@ok="open = !open" @ok="open = !open"
> >

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, RowHeightInj, inject, useVModel } from '#imports' import { EditModeInj, IsExpandedFormOpenInj, RowHeightInj, inject, useVModel, iconMap, ActiveCellInj } from '#imports'
const props = defineProps<{ const props = defineProps<{
modelValue?: string | number modelValue?: string | number
@ -32,6 +32,8 @@ const isVisible = ref(false)
const inputWrapperRef = ref<HTMLElement | null>(null) const inputWrapperRef = ref<HTMLElement | null>(null)
const inputRef = ref<HTMLTextAreaElement | null>(null) const inputRef = ref<HTMLTextAreaElement | null>(null)
const active = inject(ActiveCellInj, ref(false))
watch(isVisible, () => { watch(isVisible, () => {
if (isVisible.value) { if (isVisible.value) {
setTimeout(() => { setTimeout(() => {
@ -86,21 +88,21 @@ onClickOutside(inputWrapperRef, (e) => {
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
<NcButton <div
class="!absolute right-0 bottom-0 nc-long-text-toggle-expand !duration-0" v-if="active"
:class="{ class="!absolute right-0 bottom-0 h-6 w-5 group cursor-pointer flex justify-end gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
'top-1': rowHeight !== 1, :class="{'right-2 bottom-2':editEnabled}"
'mt-2': editEnabled, data-testid="attachment-cell-file-picker-button"
'top-0.15': rowHeight === 1,
'!hidden': isExpandedFormOpen,
}"
type="text"
size="xsmall"
@click.stop="isVisible = !isVisible" @click.stop="isVisible = !isVisible"
> >
<GeneralIcon v-if="isVisible" icon="shrink" class="nc-long-text-toggle-expand h-3.75 w-3.75 !text-xs" /> <NcTooltip placement="bottom">
<GeneralIcon v-else icon="expand" class="nc-long-text-toggle-expand h-3.75 w-3.75 !text-xs" /> <template #title>Expand</template>
</NcButton> <component
:is="iconMap.expand"
class="transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-xs"
/>
</NcTooltip>
</div>
</div> </div>
<template #overlay> <template #overlay>
<div ref="inputWrapperRef" class="flex flex-col min-w-120 min-h-70 py-3 pl-3 pr-1 expanded-cell-input"> <div ref="inputWrapperRef" class="flex flex-col min-w-120 min-h-70 py-3 pl-3 pr-1 expanded-cell-input">

31
packages/nc-gui/components/cell/attachment/index.vue

@ -64,6 +64,8 @@ const {
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, _isReadonly) const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, _isReadonly)
const active = inject(ActiveCellInj, ref(false))
const isReadonly = computed(() => { const isReadonly = computed(() => {
return isLockedMode.value || _isReadonly.value return isLockedMode.value || _isReadonly.value
}) })
@ -147,7 +149,8 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
:style="{ :style="{
height: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`, height: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}" }"
class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1" class="nc-attachment-cell relative flex color-transition flex items-center"
:class="{'justify-center': !active, 'justify-between': active}"
> >
<LazyCellAttachmentCarousel /> <LazyCellAttachmentCarousel />
@ -166,20 +169,19 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
<div <div
v-if="!isReadonly" v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }" :class="{ 'mx-auto px-4': !visibleItems.length }"
class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)" class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
data-testid="attachment-cell-file-picker-button" data-testid="attachment-cell-file-picker-button"
@click.stop="open" @click.stop="open"
> >
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom"> <NcTooltip placement="bottom">
<template #title> Click or drop a file into cell</template> <template #title> Click or drop a file into cell</template>
<div class="flex items-center gap-1"> <div v-if="active || !visibleItems.length" class="flex items-center gap-1">
<MaterialSymbolsAttachFile <MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]" class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/> />
<div <div
v-if="!visibleItems.length" v-if="!visibleItems.length"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs" class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs"
@ -187,7 +189,7 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
Add file(s) Add file(s)
</div> </div>
</div> </div>
</a-tooltip> </NcTooltip>
</div> </div>
<div v-else class="flex" /> <div v-else class="flex" />
@ -202,12 +204,12 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
}" }"
> >
<template v-for="(item, i) of visibleItems" :key="item.url || item.title"> <template v-for="(item, i) of visibleItems" :key="item.url || item.title">
<a-tooltip placement="bottom"> <NcTooltip placement="bottom">
<template #title> <template #title>
<div class="text-center w-full">{{ item.title }}</div> <div class="text-center w-full">{{ item.title }}</div>
</template> </template>
<div v-if="isImage(item.title, item.mimetype ?? item.type)"> <div v-if="isImage(item.title, item.mimetype ?? item.type)">
<div class="nc-attachment flex items-center justify-center" @click.stop="selectedImage = item"> <div class="nc-attachment flex items-center justify-center" :class="{'ml-2':active}" @click.stop="selectedImage = item">
<LazyCellAttachmentImage <LazyCellAttachmentImage
class="max-h-[1.8rem] max-w-[1.8rem]" class="max-h-[1.8rem] max-w-[1.8rem]"
:alt="item.title || `#${i}`" :alt="item.title || `#${i}`"
@ -215,29 +217,30 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
/> />
</div> </div>
</div> </div>
<div v-else class="nc-attachment flex items-center justify-center" @click="openAttachment(item)"> <div v-else class="nc-attachment flex items-center justify-center" :class="{'ml-2':active}" @click="openAttachment(item)">
<component :is="FileIcon(item.icon)" v-if="item.icon" /> <component :is="FileIcon(item.icon)" v-if="item.icon" />
<IcOutlineInsertDriveFile v-else /> <IcOutlineInsertDriveFile v-else />
</div> </div>
</a-tooltip> </NcTooltip>
</template> </template>
</div> </div>
<div <div
class="group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)" v-if="active"
class="h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
> >
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom"> <NcTooltip v-else placement="bottom">
<template #title> View attachments</template> <template #title> View attachments</template>
<component <component
:is="iconMap.expand" :is="iconMap.expand"
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]" class="transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-[0.75rem]"
@click.stop="modalVisible = true" @click.stop="modalVisible = true"
/> />
</a-tooltip> </NcTooltip>
</div> </div>
</template> </template>

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

@ -1,144 +1,80 @@
<script lang="ts" setup> <script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useGlobal } from '#imports'
const router = useRouter()
const route = router.currentRoute
const workspaceStore = useWorkspace() const workspaceStore = useWorkspace()
const { activeWorkspace, isWorkspaceOwnerOrCreator } = storeToRefs(workspaceStore) const { isWorkspaceLoading } = storeToRefs(workspaceStore)
const projectStore = useProject()
const { isSharedBase } = storeToRefs(projectStore)
const { navigateToWorkspaceSettings } = useWorkspace()
const { isUIAllowed } = useUIPermission()
const dialogOpen = ref(false) const { isSharedBase } = storeToRefs(useProject())
const openDialogKey = ref<string>('')
const dataSourcesState = ref<string>('')
const projectId = ref<string>()
const isCreateProjectOpen = ref(false) const isCreateProjectOpen = ref(false)
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) { const treeViewDom = ref<HTMLElement>()
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key || ''
dataSourcesState.value = dsState || ''
projectId.value = pId || ''
}
// todo: const isTreeViewOnScrollTop = ref(false)
const currentVersion = ref('')
const isTreeViewOnScrollTop = ref(true) const checkScrollTopMoreThanZero = () => {
const onTreeViewScrollTop = (onScrollTop: boolean) => { if (treeViewDom.value) {
isTreeViewOnScrollTop.value = !onScrollTop 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 = () => { onUnmounted(() => {
navigateToWorkspaceSettings() treeViewDom.value?.removeEventListener('scroll', checkScrollTopMoreThanZero)
} })
</script> </script>
<template> <template>
<div <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="{ :style="{
outlineWidth: '1px', outlineWidth: '1px',
height: isSharedBase ? '100%' : null,
}" }"
> >
<div class="flex flex-col"> <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"> <DashboardSidebarHeader />
<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>
<WorkspaceMenu :workspace="activeWorkspace" :is-open="true"> <DashboardSidebarTopSection v-if="!isSharedBase" />
<template #brandIcon> </div>
<div <div
v-if="!isSharedBase" ref="treeViewDom"
v-e="['c:navbar:home']" class="flex flex-col nc-scrollbar-sm-dark flex-grow"
data-testid="nc-noco-brand-icon" :class="{
class="w-[29px] min-w-[29px] nc-noco-brand-icon" 'border-t-1': !isSharedBase,
> 'border-transparent': !isTreeViewOnScrollTop,
<img width="25" class="mr-0" alt="NocoDB" src="~/assets/img/icons/512x512.png" /> 'pt-0.25': isSharedBase,
</div> }"
</template> >
</WorkspaceMenu> <div v-if="!isSharedBase" class="flex flex-row w-full justify-between items-center my-1.5 pl-4 pr-1.75">
</div> <template v-if="!isWorkspaceLoading">
</div> <div class="text-gray-500 font-medium">{{ $t('objects.projects') }}</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>
<WorkspaceCreateProjectBtn <WorkspaceCreateProjectBtn
v-if="isUIAllowed('projectCreate', false)"
v-model:is-open="isCreateProjectOpen" v-model:is-open="isCreateProjectOpen"
modal modal
type="text" type="text"
class="!p-0 mx-1" size="xxsmall"
data-testid="nc-sidebar-create-project-btn" class="!hover:bg-gray-200 !hover-text-gray-800 !text-gray-600"
:active-workspace-id="route.params.typeOrId" :centered="true"
data-testid="nc-sidebar-create-project-btn-small"
> >
<div <GeneralIcon icon="plus" class="text-lg leading-6" style="-webkit-text-stroke: 0.2px" />
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>
</WorkspaceCreateProjectBtn> </WorkspaceCreateProjectBtn>
</div> </template>
<a-skeleton-input v-else :active="true" class="mt-0.5 !w-40 !h-4 !rounded overflow-hidden" />
<div class="w-full mt-2"></div> </div>
<div class="text-gray-500 mx-5 font-medium mb-1.5">{{ $t('objects.projects') }}</div> <LazyDashboardTreeView v-if="!isWorkspaceLoading" />
<div </div>
class="w-full border-b-1" <div v-if="!isSharedBase" style="height: var(--sidebar-bottom-height)">
:class="{ <DashboardSidebarUserInfo />
'border-gray-200': !isTreeViewOnScrollTop,
'border-transparent': isTreeViewOnScrollTop,
}"
></div>
</template>
</div> </div>
<LazyDashboardTreeViewNew
class="flex-1"
:class="{
'nc-shared-base': isSharedBase,
}"
@create-base-dlg="toggleDialog(true, 'dataSources', undefined, projectId)"
@on-scroll-top="onTreeViewScrollTop"
/>
</div> </div>
</template> </template>
@ -146,8 +82,4 @@ const navigateToSettings = () => {
.nc-sidebar-top-button { .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; @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> </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 if (!table) return
project.value.isExpanded = true
if (!activeKey.value || !activeKey.value.includes(`collapse-${baseId}`)) { if (!activeKey.value || !activeKey.value.includes(`collapse-${baseId}`)) {
activeKey.value.push(`collapse-${baseId}`) activeKey.value.push(`collapse-${baseId}`)
} }
@ -200,10 +202,19 @@ const addNewProjectChildEntity = async () => {
if (isAddNewProjectChildEntityLoading.value) return if (isAddNewProjectChildEntityLoading.value) return
isAddNewProjectChildEntityLoading.value = true 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 { try {
openTableCreateDialog() openTableCreateDialog()
if (!project.value.isExpanded) { if (!project.value.isExpanded && project.value.type !== NcProjectType.DB) {
project.value.isExpanded = true project.value.isExpanded = true
} }
} finally { } finally {
@ -246,7 +257,6 @@ const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, to
} }
if (!isProjectPopulated) { if (!isProjectPopulated) {
await loadProject(project.id!)
await loadProjectTables(project.id!) await loadProjectTables(project.id!)
} }
@ -375,7 +385,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@click="onProjectClick(project, true, true)" @click="onProjectClick(project, true, true)"
> >
<PhTriangleFill <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 }" :class="{ '!rotate-180': project.isExpanded, '!visible': isOptionsOpen }"
/> />
</div> </div>
@ -444,7 +454,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<a-menu-item @click="enableEditMode"> <a-menu-item @click="enableEditMode">
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<GeneralIcon icon="edit" class="group-hover:text-black" /> <GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.edit') }} {{ $t('general.rename') }}
</div> </div>
</a-menu-item> </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)"> <a-menu-item v-if="isUIAllowed('duplicateProject', true, projectRole)" @click="duplicateProject(project)">
<div class="nc-menu-item-wrapper"> <div class="nc-menu-item-wrapper">
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('objects.project') }} {{ $t('general.duplicate') }}
</div> </div>
</a-menu-item> </a-menu-item>
@ -498,7 +508,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div> </div>
</a-menu-item> </a-menu-item>
<template v-if="project.bases && project.bases[0]"> <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 /> <a-menu-divider />
</template> </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 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 v-if="project?.bases?.[0]?.enabled" class="flex-1">
<div class="transition-height duration-200"> <div class="transition-height duration-200">
<DashboardTreeViewNewTableList :project="project" :base-index="0" /> <DashboardTreeViewTableList :project="project" :base-index="0" />
</div> </div>
</div> </div>
@ -627,7 +637,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div> </div>
</a-menu-item> </a-menu-item>
<DashboardTreeViewNewBaseOptions v-model:project="project" :base="base" /> <DashboardTreeViewBaseOptions v-model:project="project" :base="base" />
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </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'}`" :key="`sortable-${base.id}-${base.id && base.id in keys ? keys[base.id] : '0'}`"
:nc-base="base.id" :nc-base="base.id"
> >
<DashboardTreeViewNewTableList :project="project" :base-index="baseIndex" /> <DashboardTreeViewTableList :project="project" :base-index="baseIndex" />
</div> </div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
@ -726,7 +736,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
} }
:deep(.ant-collapse-header) { :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) { :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 // Todo: temp
const { isSharedBase } = useProject() const { isSharedBase } = useProject()
// const isMultiBase = computed(() => project.bases && project.bases.length > 1) // const isMultiBase = computed(() => project.bases && project.bases.length > 1)
const canUserEditEmote = computed(() => {
return isUIAllowed('tableIconCustomisation', false, projectRole?.value)
})
</script> </script>
<template> <template>
@ -98,16 +101,22 @@ const { isSharedBase } = useProject()
<template #title>{{ table.table_name }}</template> <template #title>{{ table.table_name }}</template>
<div class="table-context flex items-center gap-1 h-full" @contextmenu="setMenuContext('table', table)"> <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 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 <LazyGeneralEmojiPicker
:key="table.meta?.icon" :key="table.meta?.icon"
:emoji="table.meta?.icon" :emoji="table.meta?.icon"
size="small" size="small"
:readonly="!isUIAllowed('tableIconCustomisation', false, projectRole)" :readonly="!canUserEditEmote"
@emoji-selected="setIcon($event, table)" @emoji-selected="setIcon($event, table)"
> >
<template #default> <template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click> <NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title> <template #title>
{{ 'Change icon' }} {{ 'Change icon' }}
</template> </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 type { TableType } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import GithubButton from 'vue-github-button'
import ProjectWrapper from './ProjectWrapper.vue' import ProjectWrapper from './ProjectWrapper.vue'
import type { TabType } from '#imports' import type { TabType } from '#imports'
@ -28,10 +27,6 @@ import {
import { useRouter } from '#app' import { useRouter } from '#app'
const emit = defineEmits<{
(event: 'onScrollTop', type: boolean): void
}>()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { addTab } = useTabs() const { addTab } = useTabs()
@ -48,8 +43,12 @@ const { createProject: _createProject } = projectsStore
const { projects, projectsList, activeProjectId } = storeToRefs(projectsStore) const { projects, projectsList, activeProjectId } = storeToRefs(projectsStore)
const { isWorkspaceLoading } = storeToRefs(useWorkspace())
const { openTable } = useTablesStore() const { openTable } = useTablesStore()
const projectCreateDlg = ref(false)
const projectStore = useProject() const projectStore = useProject()
const { loadTables } = projectStore const { loadTables } = projectStore
@ -184,6 +183,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
} }
break break
} }
// ALT + D
case 68: {
e.stopPropagation()
projectCreateDlg.value = true
break
}
} }
} }
}) })
@ -203,19 +208,6 @@ provide(TreeViewInj, {
useEventListener(document, 'contextmenu', handleContext, true) 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 scrollTableNode = () => {
const activeTableDom = document.querySelector(`.nc-treeview [data-table-id="${_activeTable.value?.id}"]`) const activeTableDom = document.querySelector(`.nc-treeview [data-table-id="${_activeTable.value?.id}"]`)
if (!activeTableDom) return if (!activeTableDom) return
@ -256,56 +248,24 @@ watch(
immediate: true, immediate: true,
}, },
) )
onMounted(() => {
treeViewDom.value?.addEventListener('scroll', checkScrollTopMoreThanZero)
})
onUnmounted(() => {
treeViewDom.value?.removeEventListener('scroll', checkScrollTopMoreThanZero)
})
</script> </script>
<template> <template>
<div class="nc-treeview-container flex flex-col justify-between select-none"> <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"> <template v-if="projectsList?.length">
<ProjectWrapper <ProjectWrapper v-for="project of projectsList" :key="project.id" :project-role="project.project_role" :project="project">
v-for="project of projectsList" <DashboardTreeViewProjectNode />
:key="project.id"
:project-role="project.project_role || project.workspace_role"
:project="project"
>
<DashboardTreeViewNewProjectNode />
</ProjectWrapper> </ProjectWrapper>
</template> </template>
<WorkspaceEmptyPlaceholder v-else /> <WorkspaceEmptyPlaceholder v-else-if="!isWorkspaceLoading" />
</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" />
</div> </div>
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" />
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.nc-treeview-container {
height: calc(100% - var(--sidebar-top-height));
}
.nc-treeview-footer-item { .nc-treeview-footer-item {
@apply cursor-pointer px-4 py-2 flex items-center hover:bg-gray-200/20 text-xs text-current; @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> </style>

5
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -52,6 +52,7 @@ async function loadBases(changed?: boolean) {
try { try {
if (changed) refreshCommandPalette() if (changed) refreshCommandPalette()
await until(() => !!project.value.id).toBeTruthy()
isReloading.value = true isReloading.value = true
vReload.value = true vReload.value = true
const baseList = await $api.base.list(project.value.id as string) const baseList = await $api.base.list(project.value.id as string)
@ -164,8 +165,7 @@ const forceAwaken = () => {
onMounted(async () => { onMounted(async () => {
if (sources.value.length === 0) { if (sources.value.length === 0) {
await loadBases() loadBases()
await loadMetaDiff()
} }
}) })
@ -174,7 +174,6 @@ watch(
async (reload) => { async (reload) => {
if (reload && !isReloading.value) { if (reload && !isReloading.value) {
await loadBases() await loadBases()
await loadMetaDiff()
} }
}, },
) )

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

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

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

@ -350,8 +350,8 @@ onMounted(async () => {
// todo: replace setTimeout and follow better approach // todo: replace setTimeout and follow better approach
setTimeout(() => { setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]') const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.value.title.length) input?.setSelectionRange(0, formState.value.title.length)
input.focus() input?.focus()
}, 500) }, 500)
}) })
}) })
@ -379,6 +379,7 @@ watch(
<div class="create-base bg-white relative flex flex-col justify-center gap-2 w-full"> <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"> <h1 class="prose-2xl font-bold self-start mb-4 flex items-center gap-2">
New Base New Base
<DashboardSettingsDataSourcesInfo />
<span class="flex-grow"></span> <span class="flex-grow"></span>
</h1> </h1>

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

@ -25,9 +25,9 @@ const { modelValue, baseId } = defineProps<{
const emit = defineEmits(['update:modelValue']) 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() const { $state, $jobs } = useNuxtApp()

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

@ -33,8 +33,8 @@ const optionsToExclude = computed(() => {
const isLoading = ref(false) const isLoading = ref(false)
const _duplicate = async () => { const _duplicate = async () => {
isLoading.value = true
try { try {
isLoading.value = true
// pick a random color from array and assign to project // pick a random color from array and assign to project
const color = projectThemeColors[Math.floor(Math.random() * 1000) % projectThemeColors.length] const color = projectThemeColors[Math.floor(Math.random() * 1000) % projectThemeColors.length]
const tcolor = tinycolor(color) const tcolor = tinycolor(color)
@ -58,18 +58,28 @@ const _duplicate = async () => {
props.onOk(jobData as any) props.onOk(jobData as any)
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) 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) const isEaster = ref(false)
</script> </script>
<template> <template>
<GeneralModal v-if="project" v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-project-duplicate"> <GeneralModal v-if="project" v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-project-duplicate">
<div> <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> <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>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end"> <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="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> </div>
</GeneralModal> </GeneralModal>
</template> </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 isLoading = ref(false)
const _duplicate = async () => { const _duplicate = async () => {
isLoading.value = true
try { try {
isLoading.value = true
const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, { options: optionsToExclude.value }) const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, { options: optionsToExclude.value })
props.onOk(jobData as any) props.onOk(jobData as any)
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) 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) const isEaster = ref(false)
</script> </script>
<template> <template>
<a-modal <GeneralModal
v-model:visible="dialogShow" v-model:visible="dialogShow"
:class="{ active: dialogShow }" :class="{ active: dialogShow }"
centered centered
wrap-class-name="nc-modal-table-duplicate" wrap-class-name="nc-modal-table-duplicate"
:footer="null" :footer="null"
:closable="false" :closable="false"
class="!w-[30rem]"
@keydown.esc="dialogShow = false" @keydown.esc="dialogShow = false"
> >
<div> <div>
<div class="text-base font-medium self-center mb-4" @dblclick="isEaster = !isEaster"> <div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ table.title }} {{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div> </div>
<div class="flex flex-col gap-y-2"> <div class="mt-4">Are you sure you want to duplicate the `{{ table.title }}` table?</div>
<div class="flex flex-row gap-x-2 items-center">
<a-switch v-model:checked="options.includeData" /> <div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
Include Data
</div> <a-divider class="!m-0 !p-0 !my-2" />
<div class="flex flex-row gap-x-2 items-center">
<a-switch v-model:checked="options.includeViews" /> <div class="text-xs p-2">
Include Views <a-checkbox v-model:checked="options.includeData">Include data</a-checkbox>
</div> <a-checkbox v-model:checked="options.includeViews">Include views</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">Include hooks</a-checkbox> <a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">Include webhooks</a-checkbox>
</div> </div>
</div> </div>
<div class="flex flex-row justify-end gap-x-2 mt-8"> <div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<a-button key="back" size="middle" class="!rounded-md" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button> <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>
<a-button key="submit" size="middle" type="primary" class="!rounded-md" :loading="isLoading" @click="_duplicate"
>{{ $t('general.duplicate') }}
</a-button>
</div> </div>
</a-modal> </GeneralModal>
</template> </template>
<style scoped lang="scss"></style>

8
packages/nc-gui/components/dlg/ViewCreate.vue

@ -156,7 +156,13 @@ function init() {
} }
async function onSubmit() { async function onSubmit() {
const isValid = await formValidator.value?.validateFields() let isValid = null
try {
isValid = await formValidator.value?.validateFields()
} catch (e) {
console.error(e)
}
if (isValid && form.type) { if (isValid && form.type) {
const _meta = unref(meta) const _meta = unref(meta)

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

@ -155,11 +155,8 @@ watch(showShareModal, (val) => {
</div> </div>
<div class="share-base"> <div class="share-base">
<div class="flex flex-row items-center gap-x-2 px-4 pt-3 pb-3 select-none"> <div class="flex flex-row items-center gap-x-2 px-4 pt-3 pb-3 select-none">
<component <GeneralProjectIcon :type="project.type" class="nc-view-icon group-hover" />
:is="viewIcons[view?.type]?.icon"
class="nc-view-icon group-hover"
:style="{ color: viewIcons[view?.type]?.color }"
/>
<div>Share Base</div> <div>Share Base</div>
<div <div
class="max-w-79/100 ml-2 px-2 py-0.5 rounded-md bg-gray-100 capitalize text-ellipsis overflow-hidden" 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> <template>
<a-dropdown v-model:visible="isOpen" trigger="click" :disabled="readonly"> <a-dropdown v-model:visible="isOpen" trigger="click" :disabled="readonly">
<div <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="{ :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, 'bg-gray-500 bg-opacity-15': isOpen,
'h-6 w-6 text-lg': size === 'small', 'h-6 w-6 text-lg': size === 'small',
'h-8 w-8 text-xl': size === 'medium', 'h-8 w-8 text-xl': size === 'medium',

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

@ -12,7 +12,7 @@ const route = useRoute()
const email = computed(() => user.value?.email ?? '---') const email = computed(() => user.value?.email ?? '---')
const logout = async () => { const logout = async () => {
await signOut() await signOut(false)
navigateTo('/signin') navigateTo('/signin')
} }
</script> </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 { $api } = useNuxtApp()
const { currentVersion, latestRelease, hiddenRelease } = useGlobal() const { currentVersion, latestRelease, hiddenRelease, appInfo } = useGlobal()
const releaseAlert = computed({ const releaseAlert = computed({
get() { get() {
@ -41,14 +41,14 @@ onMounted(async () => await fetchReleaseInfo())
</script> </script>
<template> <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-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"> <div class="flex gap-1 items-center text-white">
<span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span> <span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span>
<mdi-menu-down /> <mdi-menu-down />
</div> </div>
</a-button> </NcButton>
<template #overlay> <template #overlay>
<div class="mt-1 bg-white shadow-lg !border"> <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" v-else-if="viewMeta?.type"
class="nc-view-icon group-hover" class="nc-view-icon group-hover"
:style="{ :style="{
'color': !props.ignoreColor ? viewIcons[viewMeta.type]?.color : undefined, color: !props.ignoreColor ? viewIcons[viewMeta.type]?.color : undefined,
'fontWeight': 500, fontWeight: 500,
'-webkit-text-stroke': !props.ignoreColor ? `0.5px ${viewIcons[viewMeta.type]?.color}` : '0.5px',
}" }"
/> />
</template> </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> </div>
<template #overlay> <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 /> <GeneralLanguageMenu />
</a-menu> </a-menu>
</template> </template>

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

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

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

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ButtonType } from 'ant-design-vue/lib/button' import type { ButtonType } from 'ant-design-vue/lib/button'
import { useSlots } from 'vue' import { useSlots } from 'vue'
import type { NcButtonSize } from '~/lib'
/** /**
* @description * @description
@ -17,7 +18,7 @@ interface Props {
loading?: boolean loading?: boolean
disabled?: boolean disabled?: boolean
type?: ButtonType | 'danger' | undefined type?: ButtonType | 'danger' | undefined
size?: 'xsmall' | 'small' | 'medium' size?: NcButtonSize
centered?: boolean centered?: boolean
} }
@ -32,6 +33,8 @@ const emits = defineEmits(['update:loading'])
const slots = useSlots() const slots = useSlots()
const NcButton = ref<HTMLElement | null>(null)
const size = computed(() => props.size) const size = computed(() => props.size)
const type = computed(() => props.type) const type = computed(() => props.type)
@ -62,10 +65,15 @@ const onBlur = () => {
isFocused.value = false isFocused.value = false
isClicked.value = false isClicked.value = false
} }
useEventListener(NcButton, 'mousedown', () => {
isClicked.value = true
})
</script> </script>
<template> <template>
<a-button <a-button
ref="NcButton"
:disabled="props.disabled" :disabled="props.disabled"
:loading="loading" :loading="loading"
:type="type" :type="type"
@ -74,11 +82,11 @@ const onBlur = () => {
small: size === 'small', small: size === 'small',
medium: size === 'medium', medium: size === 'medium',
xsmall: size === 'xsmall', xsmall: size === 'xsmall',
xxsmall: size === 'xxsmall',
focused: isFocused, focused: isFocused,
}" }"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@mousedown="isClicked = true"
> >
<div <div
class="flex flex-row gap-x-2.5 w-full" class="flex flex-row gap-x-2.5 w-full"
@ -91,6 +99,7 @@ const onBlur = () => {
<slot v-else name="icon" /> <slot v-else name="icon" />
<div <div
class="flex flex-row items-center"
:class="{ :class="{
'font-medium': type === 'primary' || type === 'danger', 'font-medium': type === 'primary' || type === 'danger',
}" }"
@ -120,7 +129,7 @@ const onBlur = () => {
} }
.nc-button.ant-btn { .nc-button.ant-btn {
@apply rounded-lg font-medium; @apply rounded-lg font-medium;
} }
.nc-button.ant-btn.small { .nc-button.ant-btn.small {
@ -132,7 +141,11 @@ const onBlur = () => {
} }
.nc-button.ant-btn.xsmall { .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] { .nc-button.ant-btn[disabled] {
@ -160,7 +173,7 @@ const onBlur = () => {
@apply bg-white border-1 border-gray-200 text-gray-700; @apply bg-white border-1 border-gray-200 text-gray-700;
&:hover { &:hover {
@apply bg-gray-50; @apply bg-gray-100;
} }
} }
@ -175,7 +188,7 @@ const onBlur = () => {
.nc-button.ant-btn-text { .nc-button.ant-btn-text {
box-shadow: none; 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 { &:focus {
box-shadow: none; 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> <script lang="ts" setup>
import { onKeyStroke } from '#imports'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
trigger?: Array<'click' | 'hover' | 'contextmenu'> trigger?: Array<'click' | 'hover' | 'contextmenu'>
visible?: boolean | undefined visible?: boolean | undefined
overlayClassName?: string | undefined overlayClassName?: string | undefined
autoClose?: boolean
}>(), }>(),
{ {
trigger: () => ['click'], trigger: () => ['click'],
visible: undefined, visible: undefined,
overlayClassName: undefined, overlayClassName: undefined,
autoClose: true,
}, },
) )
@ -18,8 +22,10 @@ const trigger = toRef(props, 'trigger')
const overlayClassName = toRef(props, 'overlayClassName') const overlayClassName = toRef(props, 'overlayClassName')
const autoClose = computed(() => props.autoClose)
const overlayClassNameComputed = computed(() => { 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) { if (overlayClassName.value) {
className += ` ${overlayClassName.value}` className += ` ${overlayClassName.value}`
} }
@ -27,6 +33,20 @@ const overlayClassNameComputed = computed(() => {
}) })
const visible = useVModel(props, 'visible', emits) 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> </script>
<template> <template>
@ -39,7 +59,7 @@ const visible = useVModel(props, 'visible', emits)
<slot /> <slot />
<template #overlay> <template #overlay>
<slot name="overlay" /> <slot ref="overlayWrapperDomRef" name="overlay" />
</template> </template>
</a-dropdown> </a-dropdown>
</template> </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 visible = useVModel(props, 'visible', emits)
const slots = useSlots()
</script> </script>
<template> <template>
@ -76,7 +78,7 @@ const visible = useVModel(props, 'visible', emits)
maxHeight: height, 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" /> <slot name="header" />
</div> </div>

7
packages/nc-gui/components/nc/Tabs.vue

@ -3,17 +3,10 @@ const props = defineProps<{
modelValue?: string modelValue?: string
centered?: boolean centered?: boolean
}>() }>()
const emits = defineEmits<{
(event: 'update:modelValue', data: string): void
}>()
const vModel = useVModel(props, 'modelValue', emits)
</script> </script>
<template> <template>
<a-tabs <a-tabs
v-model:activeKey="vModel"
class="nc-tabs" class="nc-tabs"
:class="{ :class="{
centered: props.centered, centered: props.centered,

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

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

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

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

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

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

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

@ -23,7 +23,13 @@ const usersData = ref<{
roles?: string roles?: string
}>() }>()
const isInvitingCollaborators = ref(false)
const inviteCollaborator = async () => { const inviteCollaborator = async () => {
isInvitingCollaborators.value = true
if (isInvitingCollaborators.value) return
try { try {
usersData.value = await inviteUser(inviteData) usersData.value = await inviteUser(inviteData)
usersData.roles = inviteData.roles usersData.roles = inviteData.roles
@ -35,6 +41,8 @@ const inviteCollaborator = async () => {
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
isInvitingCollaborators.value = false
} }
const inviteUrl = computed(() => const inviteUrl = computed(() =>
@ -111,7 +119,7 @@ const copyUrl = async () => {
class="!max-w-130 !rounded" 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> <template #suffixIcon>
<MdiChevronDown /> <MdiChevronDown />
</template> </template>
@ -140,7 +148,7 @@ const copyUrl = async () => {
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
</a-select-option> </a-select-option>
</a-select> </NcSelect>
<a-button <a-button
type="primary" type="primary"
@ -162,10 +170,14 @@ const copyUrl = async () => {
<style scoped> <style scoped>
.badge-text { .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) { :deep(.ant-select .ant-select-selector) {
@apply rounded; @apply rounded;
} }
:deep(.ant-select-selection-item) {
@apply mt-0.75;
}
</style> </style>

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

@ -57,10 +57,11 @@ watch(
<template> <template>
<div class="h-full nc-project-view"> <div class="h-full nc-project-view">
<div <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 }" :class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
> >
<div class="flex flex-row items-center gap-x-4"> <div class="flex flex-row items-center gap-x-4">
<GeneralOpenLeftSidebarBtn />
<GeneralProjectIcon :type="openedProject?.type" /> <GeneralProjectIcon :type="openedProject?.type" />
<div class="flex font-medium text-base capitalize"> <div class="flex font-medium text-base capitalize">
{{ openedProject?.title }} {{ openedProject?.title }}

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

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

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

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PaginatedType } from 'nocodb-sdk' import type { PaginatedType } from 'nocodb-sdk'
import SidebarIcon from '~icons/nc-icons/sidebar'
import { IsGroupByInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports' import { IsGroupByInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports'
import type { Language } 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;' : '' isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : ''
}${extraStyle}`" }${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" /> <slot name="add-record" />
<div class="flex-1"> <div class="flex-1">
<span <span
@ -117,7 +98,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</div> </div>
</template> </template>
<div class="flex-1 text-right pr-2"> <div class="flex-1 text-right">
<span <span
v-if="alignCountOnRight && count !== null && count !== Infinity" v-if="alignCountOnRight && count !== null && count !== Infinity"
class="caption nc-grid-row-count mr-2.5 text-gray-500 text-xs" 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') }} {{ count }} {{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span> </span>
</div> </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> </div>
</template> </template>

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

@ -12,12 +12,21 @@ const { isMobileMode } = useGlobal()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { allowCSVDownload } = useSharedView() const { allowCSVDownload } = useSharedView()
const isViewSidebarAvailable = computed(
() => (isGrid.value || isGallery.value || isKanban.value || isMap.value) && !isPublic.value,
)
</script> </script>
<template> <template>
<div <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 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, 'h-[var(--topbar-height)]': !isMobileMode }" :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" style="z-index: 7"
> >
<template v-if="isViewsLoading"> <template v-if="isViewsLoading">
@ -50,6 +59,7 @@ const { allowCSVDownload } = useSharedView()
v-if="(isGrid || isGallery || isKanban || isMap) && !isPublic && isUIAllowed('dataInsert')" v-if="(isGrid || isGallery || isKanban || isMap) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false" :show-system-fields="false"
/> />
<LazySmartsheetToolbarOpenViewSidebarBtn v-if="isViewSidebarAvailable" />
</template> </template>
</div> </div>
</template> </template>

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

@ -22,13 +22,17 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<template> <template>
<div <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-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" style="z-index: 7"
> >
<template v-if="isViewsLoading"> <template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" /> <a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" />
</template> </template>
<template v-else> <template v-else>
<GeneralOpenLeftSidebarBtn />
<LazySmartsheetToolbarViewInfo v-if="!isPublic" /> <LazySmartsheetToolbarViewInfo v-if="!isPublic" />
<div v-if="!isMobileMode" class="flex-1" /> <div v-if="!isMobileMode" class="flex-1" />
@ -38,6 +42,11 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<GeneralApiLoader /> <GeneralApiLoader />
<LazyGeneralShareProject v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic" is-view-toolbar /> <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> </template>
</div> </div>
</template> </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-form-item label="NN">
<a-checkbox <a-checkbox
v-model:checked="vModel.rqd" v-model:checked="vModel.rqd"
:disabled="vModel.pk" :disabled="vModel.pk || !sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-NN" class="nc-column-checkbox-NN"
@change="onAlter" @change="onAlter"
/> />
@ -59,6 +59,7 @@ vModel.value.au = !!vModel.value.au */
<a-form-item label="PK"> <a-form-item label="PK">
<a-checkbox <a-checkbox
v-model:checked="vModel.pk" v-model:checked="vModel.pk"
:disabled="!sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-PK" class="nc-column-checkbox-PK"
@change="onAlter" @change="onAlter"
/> />
@ -67,17 +68,17 @@ vModel.value.au = !!vModel.value.au */
<a-form-item label="AI"> <a-form-item label="AI">
<a-checkbox <a-checkbox
v-model:checked="vModel.ai" v-model:checked="vModel.ai"
:disabled="sqlUi.colPropUNDisabled(vModel)" :disabled="sqlUi.colPropUNDisabled(vModel) || !sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-AI" class="nc-column-checkbox-AI"
@change="onAlter" @change="onAlter"
/> />
</a-form-item> </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-checkbox v-model:checked="vModel.un" class="nc-column-checkbox-UN" />
</a-form-item> </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-checkbox v-model:checked="vModel.au" class="nc-column-checkbox-AU" />
</a-form-item> </a-form-item>
</div> </div>
@ -95,13 +96,13 @@ vModel.value.au = !!vModel.value.au */
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')"> <a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input <a-input
v-model:value="vModel.dtxp" v-model:value="vModel.dtxp"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt)" :disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)"
@input="onAlter" @input="onAlter"
/> />
</a-form-item> </a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale"> <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> </a-form-item>
</template> </template>

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

@ -49,7 +49,7 @@ const { copy } = useCopy()
const copyRecordUrl = () => { const copyRecordUrl = () => {
copy( copy(
encodeURI( 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}` : '' props.view ? `/${props.view.title}` : ''
}?rowId=${primaryKey.value}`, }?rowId=${primaryKey.value}`,
), ),
@ -127,12 +127,18 @@ const onConfirmDeleteRowClick = async () => {
/> />
</a-tooltip> </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') }} {{ $t('general.save') }}
</a-button> </NcButton>
<a-dropdown> <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> <template #overlay>
<a-menu> <a-menu>
<a-menu-item v-if="!isNew" @click="loadRow()"> <a-menu-item v-if="!isNew" @click="loadRow()">
@ -174,9 +180,17 @@ const onConfirmDeleteRowClick = async () => {
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick"> <GeneralModal v-model:visible="showDeleteRowModal" class="!w-[25rem]">
<p>Are you sure you want to delete this row?</p> <div class="p-4">
</a-modal> <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> </div>
</template> </template>

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

@ -1124,7 +1124,7 @@ defineExpose({
}) })
// when expand is clicked the drawer should open // 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>) => { const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
if (expandForm) { if (expandForm) {
expandForm(row, col) 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"> <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" /> <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> </div>
<template v-if="isEeUI && persistMenu" #overlay> <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"> <td class="text-left pointer sticky left-0 !border-r-0">
<div class="px-2 w-full flex items-center text-gray-500"> <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> </div>
</td> </td>
<td class="!border-gray-100" :colspan="visibleColLength"></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" :extra-style="paginationStyleRef?.extraStyle"
> >
<template #add-record> <template #add-record>
<div v-if="isAddingEmptyRowAllowed" class="flex ml-2"> <div v-if="isAddingEmptyRowAllowed" class="flex ml-1">
<a-dropdown-button <a-dropdown-button
class="nc-grid-add-new-row" class="nc-grid-add-new-row"
placement="top" placement="top"

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

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

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

@ -297,13 +297,38 @@ const setIcon = async (icon: string, view: ViewType) => {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }
const scrollViewNode = () => {
const activeViewDom = document.querySelector(`.nc-views-menu [data-view-id="${activeView.value?.id}"]`) as HTMLElement
if (!activeViewDom) return
if (isElementInvisible(activeViewDom)) {
// Scroll to the view node
activeViewDom?.scrollIntoView({ behavior: 'auto', inline: 'start' })
}
}
watch(
() => activeView.value?.id,
() => {
if (!activeView.value?.id) return
// TODO: Find a better way to scroll to the view node
setTimeout(() => {
scrollViewNode()
}, 800)
},
{
immediate: true,
},
)
</script> </script>
<template> <template>
<a-menu <a-menu
ref="menuRef" ref="menuRef"
:class="{ dragging }" :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 w-full !border-r-0 !bg-inherit"
:selected-keys="selected" :selected-keys="selected"
> >
<!-- Lazy load breaks menu item active styles, i.e. styles never change even when active item changes --> <!-- Lazy load breaks menu item active styles, i.e. styles never change even when active item changes -->
@ -319,6 +344,7 @@ const setIcon = async (icon: string, view: ViewType) => {
'active': activeView?.id === view.id, 'active': activeView?.id === view.id,
[`nc-${view.type ? viewTypeAlias[view.type] : undefined || view.type}-view-item`]: true, [`nc-${view.type ? viewTypeAlias[view.type] : undefined || view.type}-view-item`]: true,
}" }"
:data-view-id="view.id"
@change-view="changeView" @change-view="changeView"
@open-modal="$emit('openModal', $event)" @open-modal="$emit('openModal', $event)"
@delete="openDeleteDialog" @delete="openDeleteDialog"

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

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

@ -16,9 +16,6 @@ import {
useViewsStore, useViewsStore,
watch, watch,
} from '#imports' } from '#imports'
import FieldIcon from '~icons/nc-icons/eye'
const openedTab = ref<'views' | 'developer'>('views')
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
@ -50,12 +47,10 @@ const route = useRoute()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { rightSidebarSize } = storeToRefs(useSidebarStore()) const { isRightSidebarOpen } = storeToRefs(useSidebarStore())
const tabBtnsContainerRef = ref<HTMLElement | null>(null) const tabBtnsContainerRef = ref<HTMLElement | null>(null)
const minimalMode = ref(false)
/** Watch route param and change active view based on `viewTitle` */ /** Watch route param and change active view based on `viewTitle` */
watch( watch(
[views, () => route.params.viewTitle], [views, () => route.params.viewTitle],
@ -142,40 +137,6 @@ function onOpenModal({
close(1000) 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> </script>
<template> <template>
@ -186,14 +147,46 @@ onUnmounted(() => {
<div <div
v-else v-else
ref="tabBtnsContainerRef" 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>
<div class="flex-1 flex flex-col min-h-0"> <div class="flex-1 flex flex-col min-h-0">
<div class="flex flex-col h-full justify-between w-full"> <div class="flex flex-col h-full justify-between w-full">
<div class="flex flex-grow w-full"> <div class="flex flex-grow nc-scrollbar-md pr-1.75 mr-0.5">
<div v-if="isViewsLoading" class="flex flex-col w-full"> <div v-if="isViewsLoading" class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mt-1.5 ml-5 gap-x-3"> <div class="flex flex-row items-center w-full mt-1.5 ml-5 gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" /> <a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />

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

@ -60,7 +60,7 @@ useMenuCloseOnEsc(open)
</script> </script>
<template> <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 }"> <div :class="{ 'nc-active-btn': filtersLength }">
<a-button v-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked"> <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"> <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-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> <a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<component :is="iconMap.download" class="group-hover:text-accent text-gray-600" /> <component :is="iconMap.download" class="group-hover:text-accent text-gray-500" />
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('general.download') }}</span> <span class="text-capitalize !text-sm font-medium text-gray-500">{{ $t('general.download') }}</span>
<component :is="iconMap.arrowDown" class="text-grey" /> <component :is="iconMap.arrowDown" class="text-grey" />
</div> </div>
</a-button> </a-button>

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

@ -278,7 +278,7 @@ useMenuCloseOnEsc(open)
</script> </script>
<template> <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 }"> <div :class="{ 'nc-active-btn': numberOfHiddenFields }">
<a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked"> <a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">

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

@ -142,7 +142,12 @@ watch(open, () => {
</script> </script>
<template> <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
:trigger="['click']"
overlay-class-name="nc-dropdown-group-by-menu nc-toolbar-dropdown"
>
<div :class="{ 'nc-badge nc-active-btn': groupedByColumnIds?.length }"> <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"> <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"> <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(() => { 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 return columns.value?.find((column) => column.primaryValue)?.label
}) })
watch( 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( watchDebounced(
@ -81,29 +85,32 @@ watchDebounced(
<template> <template>
<div <div
class="flex flex-row border-1 rounded-lg h-8 ml-1 border-gray-200 overflow-hidden" 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 <div
ref="searchDropdown" ref="searchDropdown"
class="flex items-center group relative px-2 cursor-pointer border-r-1 border-gray-200 hover:bg-gray-100" 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" @click="isDropdownOpen = !isDropdownOpen"
> >
<GeneralIcon icon="search" class="ml-1 h-3.5 w-3.5 text-gray-500 group-hover:text-black" /> <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">
<component :is="iconMap.arrowDown" class="ml-1 text-gray-400 !text-sm" /> {{ displayColumnLabel }}
</div>
<div class="hidden group-hover:block">
<component :is="iconMap.arrowDown" class="text-gray-400 text-sm" />
</div>
<a-select <a-select
v-model:value="search.field" v-model:value="search.field"
:open="isDropdownOpen" :open="isDropdownOpen"
size="small" size="small"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
dropdown-class-name="!rounded-lg nc-dropdown-toolbar-search-field-option !w-48" 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" 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"> <a-select-option v-for="op of columns" :key="op.value" :value="op.value">
<div class="flex items-center -ml-1 gap-2"> <div class="text-[0.75rem] flex items-center -ml-1 gap-2">
<SmartsheetHeaderIcon class="" :column="op.column" /> <SmartsheetHeaderIcon class="text-sm" :column="op.column" />
{{ op.label }} {{ op.label }}
</div> </div>
</a-select-option> </a-select-option>
@ -113,18 +120,17 @@ watchDebounced(
<a-input <a-input
v-model:value="search.query" v-model:value="search.query"
size="small" size="small"
class="!text-xs" class="text-xs"
:style="{ :style="{
width: '12rem', width: '10rem',
}" }"
:placeholder="`${$t('general.search')} in ${displayColumnLabel}`" :placeholder="$t('general.search')"
:bordered="false" :bordered="false"
data-testid="search-data-input" data-testid="search-data-input"
@press-enter="onPressEnter" @press-enter="onPressEnter"
@focus="isFocused = true" @focus="isFocused = true"
@blur="isFocused = false" @blur="isFocused = false"
> >
<template #addonBefore> </template>
</a-input> </a-input>
</div> </div>
</template> </template>

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

@ -108,7 +108,7 @@ watch(open, () => {
</script> </script>
<template> <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 }"> <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"> <a-button v-e="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -167,7 +167,12 @@ watch(open, () => {
</template> </template>
</div> </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"> <NcButton class="!text-brand-500" type="text" size="small" @click.stop="showCreateSort = true">
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<component :is="iconMap.plus" /> <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' import { ActiveViewInj, inject } from '#imports'
const selectedView = inject(ActiveViewInj) const selectedView = inject(ActiveViewInj)
const { openedViewsTab } = storeToRefs(useViewsStore())
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
</script> </script>
<template> <template>
<div <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="{ :class="{
'max-w-2/5': selectedView?.type !== ViewTypes.KANBAN, 'min-w-2/5 max-w-2/5': selectedView?.type !== ViewTypes.KANBAN,
'max-w-1/4': 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"> <LazyGeneralEmojiPicker :emoji="activeTable?.meta?.icon" readonly size="xsmall">
{{ activeTable?.meta?.icon }} <template #default>
</div> <MdiTable class="min-w-5 !text-gray-500" :class="{}" />
<MdiTable v-else class="min-w-5 !text-gray-500 mb-0.25" :class="{}" /> </template>
</LazyGeneralEmojiPicker>
<span <span
class="text-ellipsis overflow-hidden pl-1 text-gray-500 max-w-1/2" class="text-ellipsis overflow-hidden pl-1 text-gray-500 max-w-1/2"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
@ -27,17 +29,15 @@ const { activeTable } = storeToRefs(useTablesStore())
</span> </span>
<div class="px-2 text-gray-500">/</div> <div class="px-2 text-gray-500">/</div>
<div v-if="selectedView?.meta?.icon" class="text-lg mr-0.5"> <LazyGeneralEmojiPicker :emoji="selectedView?.meta?.icon" readonly size="xsmall">
{{ selectedView?.meta?.icon }} <template #default>
</div> <GeneralViewIcon :meta="{ type: selectedView?.type }" class="min-w-4.5 text-lg flex" />
<GeneralViewIcon v-else :meta="{ type: selectedView?.type }" class="min-w-5 flex" /> </template>
</LazyGeneralEmojiPicker>
<span <span class="truncate pl-1.25 text-gray-700 max-w-28/100">
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' }"
>
{{ selectedView?.title }} {{ selectedView?.title }}
</span> </span>
<LazySmartsheetToolbarReload /> <LazySmartsheetToolbarReload v-if="openedViewsTab === 'view'" />
</div> </div>
</template> </template>

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

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

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

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

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

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

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

@ -41,14 +41,14 @@ 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 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="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> <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> <template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" /> <PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template> </template>
</a-input> </a-input>
</div> </div>
<div v-if="!filterCollaborators?.length" class="w-full h-full flex flex-col items-center justify-center mt-36"> <div v-if="!filterCollaborators?.length" class="w-full h-full flex flex-col items-center justify-center mt-36">
<Empty description="No collaborators found" /> <a-empty description="No collaborators found" />
</div> </div>
<table v-else class="nc-collaborators-list-table !nc-scrollbar-md"> <table v-else class="nc-collaborators-list-table !nc-scrollbar-md">
<thead> <thead>
@ -109,7 +109,7 @@ const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
</NcBadge> </NcBadge>
</a-select-option> </a-select-option>
<template v-for="role of accessibleRoles" :key="`role-option-${role}`"> <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]"> <NcBadge :color="RoleColors[role]">
<p class="badge-text">{{ RoleLabels[role] }}</p> <p class="badge-text">{{ RoleLabels[role] }}</p>
</NcBadge> </NcBadge>
@ -232,4 +232,8 @@ tbody {
background: rgb(203, 203, 203); background: rgb(203, 203, 203);
} }
} }
:deep(.ant-select-selection-item) {
@apply mt-0.75;
}
</style> </style>

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

@ -1,61 +1,42 @@
<script setup lang="ts"> <script setup lang="ts">
import { NcProjectType, useRouter } from '#imports' import type { NcButtonSize } from '~/lib'
const props = defineProps<{ const props = defineProps<{
activeWorkspaceId?: string | undefined activeWorkspaceId?: string | undefined
modal?: boolean modal?: boolean
type?: string type?: string
isOpen: boolean 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 projectCreateDlg = ref(false)
const projectType = ref(NcProjectType.DB)
const size = computed(() => props.size || 'small')
const navigateToCreateProject = (type: NcProjectType) => { const centered = computed(() => props.centered ?? true)
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
}
}
}
})
</script> </script>
<template> <template>
<div> <NcButton
<a-button v-if="isUIAllowed('projectCreate', false, workspaceRoles ?? orgRoles) && !isSharedBase"
class="!py-0 !px-0 !border-0 !h-full !rounded-md w-full hover:bg-gray-100 text-sm select-none cursor-pointer" type="text"
:type="props.type ?? 'primary'" :size="size"
@click="navigateToCreateProject(NcProjectType.DB)" :centered="centered"
> @click="projectCreateDlg = true"
<div class="flex w-full items-center gap-2"> >
<slot>{{ $t('title.newProj') }} <MdiMenuDown /></slot> <slot />
</div> <WorkspaceCreateProjectDlg v-model="projectCreateDlg" />
</a-button> </NcButton>
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" :type="projectType" />
</div>
</template> </template>
<style scoped></style> <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 if (n === o && !n) return
// Clear errors // Clear errors
form.value?.resetFields() setTimeout(async () => {
form.value?.resetFields()
formState.value = { formState.value = {
title: 'Untitled Database', title: 'Untitled Database',
} }
await nextTick()
input.value?.$el?.focus() await nextTick()
input.value?.$el?.select()
input.value?.$el?.focus()
input.value?.$el?.select()
}, 5)
}) })
const typeLabel = computed(() => { const typeLabel = computed(() => {
switch (props.type) { switch (props.type) {
case NcProjectType.DB: case NcProjectType.DB:
return 'Database'
default: default:
return '' return 'Database'
} }
}) })
</script> </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 projectCreateDlg = ref(false)
const projectType = ref() 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) => { const openCreateProjectDlg = (type: NcProjectType) => {
projectType.value = type projectType.value = type
@ -35,7 +16,7 @@ const openCreateProjectDlg = (type: NcProjectType) => {
<template> <template>
<div class="flex items-center justify-center h-full"> <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 /> <NcIconsInbox />
<div class="font-weight-medium">No Projects</div> <div class="font-weight-medium">No Projects</div>
<template v-if="props.buttons"> <template v-if="props.buttons">

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

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

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

@ -1,371 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup></script>
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>
<template> <template>
<div class="flex-grow min-w-20"> <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)">
<a-dropdown <div class="flex-grow min-w-20">
v-model:visible="isWorkspaceDropdownOpen"
class="h-full min-w-0 flex-1"
:trigger="['click']"
placement="bottom"
overlay-class-name="nc-dropdown-workspace-menu"
>
<div <div
:style="{ width: props.isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[props.isOpen ? '' : 'justify-center']"
data-testid="nc-workspace-menu" 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" /> <a
<template v-if="props.isOpen"> class="w-10 min-w-10 transition-all duration-200 p-1 transform"
Nocodb href="https://github.com/nocodb/nocodb"
<div class="flex flex-grow"></div> target="_blank"
<MdiCodeTags class="min-w-[17px] text-md transform rotate-90" /> >
</template> <img width="25" alt="NocoDB" src="~/assets/img/icons/256x256.png" />
</a>
<template v-else>
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" /> <div class="font-semibold text-base">Nocodb</div>
</template> <div class="flex flex-grow"></div>
</div> </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> </template>
<style scoped lang="scss"> <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 { .nc-workspace-menu-item {
@apply flex items-center pl-2 py-2 gap-2 text-sm hover:text-black; @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> </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> <script lang="ts" setup>
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
import type { WorkspaceType } from 'nocodb-sdk' import type { WorkspaceType } from 'nocodb-sdk'
import { isEeUI } from '#imports'
const router = useRouter() const router = useRouter()
const route = router.currentRoute 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() const { appInfo } = useGlobal()
@ -14,6 +17,7 @@ const tab = computed({
return route.value.query?.tab ?? 'collaborators' return route.value.query?.tab ?? 'collaborators'
}, },
set(tab: string) { set(tab: string) {
if (tab === 'collaborators') loadCollaborators()
router.push({ query: { ...route.value.query, tab } }) router.push({ query: { ...route.value.query, tab } })
}, },
}) })
@ -33,15 +37,27 @@ watch(
immediate: true, immediate: true,
}, },
) )
onMounted(() => {
until(() => activeWorkspace.value?.id)
.toMatch((v) => !!v)
.then(() => {
until(() => workspaces.value)
.toMatch((v) => v.has(activeWorkspace.value.id))
.then(() => {
loadCollaborators()
})
})
})
</script> </script>
<template> <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"> <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) }"> <span class="nc-workspace-avatar !w-8 !h-8" :style="{ backgroundColor: getWorkspaceColor(activeWorkspace) }">
{{ activeWorkspace?.title?.slice(0, 2) }} {{ activeWorkspace?.title?.slice(0, 2) }}
</span> </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 }} {{ activeWorkspace?.title }}
</h1> </h1>
</div> </div>
@ -59,7 +75,7 @@ watch(
</a-tab-pane> </a-tab-pane>
</template> </template>
<template v-if="isWorkspaceOwner && appInfo.ee"> <template v-if="isWorkspaceOwner && isEeUI">
<a-tab-pane key="billing" class="w-full"> <a-tab-pane key="billing" class="w-full">
<template #tab> <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">
@ -70,15 +86,15 @@ watch(
<WorkspaceBilling /> <WorkspaceBilling />
</a-tab-pane> </a-tab-pane>
</template> </template>
<template v-if="isWorkspaceOwner && appInfo.ee"> <template v-if="isWorkspaceOwner && isEeUI">
<a-tab-pane key="settings" class="w-full"> <a-tab-pane key="settings" class="w-full">
<template #tab> <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" /> <GeneralIcon icon="settings" />
Settings Settings
</div> </div>
</template> </template>
<WorkspaceDelete /> <WorkspaceSettings />
</a-tab-pane> </a-tab-pane>
</template> </template>
</NcTabs> </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' const DbNotFoundMsg = 'Database config not found'
let refreshTokenPromise: Promise<string> | null = null
export function addAxiosInterceptors(api: Api<any>) { export function addAxiosInterceptors(api: Api<any>) {
const state = useGlobal() const state = useGlobal()
const router = useRouter() const router = useRouter()
@ -51,6 +53,40 @@ export function addAxiosInterceptors(api: Api<any>) {
return Promise.reject(error) 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 // Try request again with new token
return api.instance return api.instance
.post('/auth/token/refresh', null, { .post('/auth/token/refresh', null, {
@ -62,6 +98,10 @@ export function addAxiosInterceptors(api: Api<any>) {
config.headers['xc-auth'] = token.data.token config.headers['xc-auth'] = token.data.token
state.signIn(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) => { return new Promise((resolve, reject) => {
api.instance api.instance
.request(config) .request(config)
@ -73,11 +113,15 @@ export function addAxiosInterceptors(api: Api<any>) {
}) })
}) })
}) })
.catch(async (error) => { .catch(async (refreshTokenError) => {
await state.signOut() await state.signOut()
if (!route.value.meta.public) navigateTo('/signIn') if (!route.value.meta.public) navigateTo('/signIn')
// reject the refresh token promise and reset
refreshTokenPromiseRej(refreshTokenError)
refreshTokenPromise = null
return Promise.reject(error) return Promise.reject(error)
}) })
}, },

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

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

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

@ -208,7 +208,6 @@ export function useData(args: {
}, },
undo: { undo: {
fn: async function undo(toUpdate: Row, property: string, pg: { page: number; pageSize: number }) { fn: async function undo(toUpdate: Row, property: string, pg: { page: number; pageSize: number }) {
console.log('undo', toUpdate, property, pg)
const updatedData = await updateRowProperty( const updatedData = await updateRowProperty(
{ row: toUpdate.oldRow, oldRow: toUpdate.row, rowMeta: toUpdate.rowMeta }, { row: toUpdate.oldRow, oldRow: toUpdate.row, rowMeta: toUpdate.rowMeta },
property, property,
@ -218,12 +217,10 @@ export function useData(args: {
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) {
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]), formattedData.value) const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]), formattedData.value)
if (rowIndex !== -1) { if (rowIndex !== -1) {
console.log('manual')
const row = formattedData.value[rowIndex] const row = formattedData.value[rowIndex]
Object.assign(row.row, updatedData) Object.assign(row.row, updatedData)
Object.assign(row.oldRow, updatedData) Object.assign(row.oldRow, updatedData)
} else { } else {
console.log('fallback')
await callbacks?.loadData?.() await callbacks?.loadData?.()
} }
} else { } 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 */ /** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = async () => { const signOut: Actions['signOut'] = async (_skipRedirect = false) => {
try { try {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
await nuxtApp.$api.auth.signout() await nuxtApp.$api.auth.signout()
@ -80,24 +80,59 @@ export function useGlobalActions(state: State): Actions {
workspaceId: _workspaceId, workspaceId: _workspaceId,
type: _type, type: _type,
projectId, projectId,
query,
}: { }: {
workspaceId?: string workspaceId?: string
projectId?: string projectId?: string
type?: NcProjectType type?: NcProjectType
query?: any
}) => { }) => {
const workspaceId = _workspaceId || 'nc' const workspaceId = _workspaceId || 'nc'
let path: string let path: string
const queryParams = query ? `?${new URLSearchParams(query).toString()}` : ''
if (projectId) { if (projectId) {
path = `/${workspaceId}/${projectId}` path = `/${workspaceId}/${projectId}${queryParams}`
} else { } else {
path = `/${workspaceId}` path = `/${workspaceId}${queryParams}`
} }
if (state.appInfo.value.baseHostName && location.hostname !== `${workspaceId}.${state.appInfo.value.baseHostName}`) { navigateTo({
location.href = `https://${workspaceId}.${state.appInfo.value.baseHostName}/dashboard/#${path}` 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 { } else {
navigateTo(path) path = `/${workspaceId}${queryParams}`
} }
navigateTo({
path,
})
} }
const getBaseUrl = (workspaceId: string) => { const getBaseUrl = (workspaceId: string) => {
@ -107,5 +142,5 @@ export function useGlobalActions(state: State): Actions {
return undefined 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 */ /** current token ref, used by `useJwt` to reactively parse our token payload */
const token = computed({ const token = computed({
get: () => storage.value.token || '', get: () => storage.value.token || '',
set: (val) => (storage.value.token = val), set: (val) => {
storage.value.token = val
},
}) })
const config = useRuntimeConfig() const config = useRuntimeConfig()
@ -105,6 +107,7 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
isCloud: false, isCloud: false,
automationLogLevel: 'OFF', automationLogLevel: 'OFF',
disableEmailAuth: false, disableEmailAuth: false,
dashboardPath: '/dashboard',
}) })
/** reactive token payload */ /** reactive token payload */

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

@ -28,6 +28,7 @@ export interface AppInfo {
baseHostName?: string baseHostName?: string
disableEmailAuth: boolean disableEmailAuth: boolean
mainSubDomain?: string mainSubDomain?: string
dashboardPath: string
} }
export interface StoredState { export interface StoredState {
@ -61,12 +62,20 @@ export interface Getters {
} }
export interface Actions { export interface Actions {
signOut: () => void signOut: (skipRedirect?: boolean) => void
signIn: (token: string) => void signIn: (token: string) => void
refreshToken: () => void refreshToken: () => void
loadAppInfo: () => void loadAppInfo: () => void
setIsMobileMode: (isMobileMode: boolean) => 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 getBaseUrl: (workspaceId: string) => string | undefined
} }

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

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

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

@ -1,5 +1,5 @@
import { isString } from '@vue/shared' import { extractRolesObj } from 'nocodb-sdk'
import { computed, createSharedComposable, ref, useApi, useGlobal } from '#imports' import { computed, createSharedComposable, useApi, useGlobal } from '#imports'
import type { ProjectRole, Role, Roles } 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) * * `projectRoles` - the roles a user has in the current project (if one was loaded)
* * `allRoles` - all roles a user has (userRoles + projectRoles) * * `allRoles` - all roles a user has (userRoles + projectRoles)
* * `hasRole` - a function to check if a user has a specific role * * `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(() => { export const useRoles = createSharedComposable(() => {
const { user, previewAs } = useGlobal() const { user, previewAs } = useGlobal()
const { api } = useApi() const { api } = useApi()
const projectRoles = ref<Roles<ProjectRole>>({}) const allRoles = computed<Roles | null>(() => {
let orgRoles = user.value?.roles ?? {}
const userRoles = computed<Roles<Role>>(() => { orgRoles = extractRolesObj(orgRoles)
let roles = user.value?.roles ?? {}
// if string populate key-value paired object let projectRoles = user.value?.project_roles ?? {}
if (isString(roles)) {
roles = roles.split(',').reduce<Roles>((acc, role) => { projectRoles = extractRolesObj(projectRoles)
acc[role] = true
return acc return {
}, {}) ...orgRoles,
...projectRoles,
} }
})
const orgRoles = computed<Roles | null>(() => {
let orgRoles = user.value?.roles ?? {}
return roles orgRoles = extractRolesObj(orgRoles)
return orgRoles
}) })
const allRoles = computed<Roles>(() => ({ const projectRoles = computed<Roles | null>(() => {
...userRoles.value, let projectRoles = user.value?.project_roles ?? {}
...projectRoles.value,
})) if (Object.keys(projectRoles).length === 0) {
projectRoles = user.value?.roles ?? {}
}
async function loadProjectRoles( projectRoles = extractRolesObj(projectRoles)
projectId: string,
return projectRoles
})
const workspaceRoles = computed<Roles | null>(() => {
return null
})
async function loadRoles(
projectId?: string,
options: { isSharedBase?: boolean; sharedBaseId?: string; isSharedErd?: boolean; sharedErdId?: string } = {}, options: { isSharedBase?: boolean; sharedBaseId?: string; isSharedErd?: boolean; sharedErdId?: string } = {},
) { ) {
if (options?.isSharedBase) { if (options?.isSharedBase) {
const user = await api.auth.me( const res = await api.auth.me(
{ {
project_id: projectId, 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) { } else if (options?.isSharedErd) {
const user = await api.auth.me( const res = await api.auth.me(
{ {
project_id: projectId, 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) { } else if (projectId) {
const user = await api.auth.me({ project_id: projectId }) const res = await api.auth.me({ project_id: projectId })
projectRoles.value = user.roles
} else { user.value = {
projectRoles.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.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) const password = useState<string | undefined>('password', () => undefined)
provide(SharedViewPasswordInj, password)
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false) const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = useState<TableType | KanbanType | MapType | undefined>('meta', () => undefined) 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({ await navigateTo({
path: `/${workspaceIdOrType}/${projectIdOrBaseId}/table/${table?.id}`, path: `/${workspaceIdOrType}/${projectIdOrBaseId}/${table?.id}`,
query: route.value.query, query: route.value.query,
}) })

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

@ -25,12 +25,22 @@ export const useUndoRedo = createSharedComposable(() => {
return tempScope return tempScope
}) })
const isSameScope = (sc: { key: string; param: string }[]) => { const isSameScope = (sc: { key: string; param: string | string[] }[]) => {
return sc.every((s) => { return sc.every((s) => {
return scope.value.some( return scope.value.some(
// viewTitle is optional for default view // viewTitle is optional for default view
(s2) => (s2) => {
(s.key === 'viewTitle' && s2.key === 'viewTitle' && s2.param === '') || (s.key === s2.key && s.param === s2.param), 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) { if (param.view) {
return [ return [
{ key: 'projectId', param: param.view.project_id! }, { key: 'projectId', param: param.view.project_id! },
{ key: 'viewId', param: param.view.fk_model_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 { } else {
return [ return [
{ key: 'projectId', param: param.project_id! }, { key: 'projectId', param: param.project_id! },
{ key: 'viewId', param: param.model_id! }, { key: 'viewId', param: param.model_id! },
{ key: 'viewTitle', param: param.title! }, { key: 'viewTitle', param: [param.title!, param.id!] },
] ]
} }
} }

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

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, iconMap, navigateTo, ref, useGlobal, useNuxtApp, useRoute, useSidebar } from '#imports' 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 }) useSidebar('nc-left-sidebar', { hasSidebar: false })
@ -14,7 +14,7 @@ const hasSider = ref(false)
const sidebar = ref<HTMLDivElement>() const sidebar = ref<HTMLDivElement>()
const logout = async () => { const logout = async () => {
await signOut() await signOut(false)
navigateTo('/signin') navigateTo('/signin')
} }
@ -68,7 +68,7 @@ hooks.hook('page:finish', () => {
<LazyGeneralReleaseInfo /> <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> <template #title> Switch language</template>
<div class="flex pr-4 items-center"> <div class="flex pr-4 items-center">
@ -128,7 +128,7 @@ hooks.hook('page:finish', () => {
</template> </template>
</a-layout-header> </a-layout-header>
<a-tooltip placement="bottom"> <a-tooltip v-if="!appInfo.ee" placement="bottom">
<template #title> Switch language</template> <template #title> Switch language</template>
<LazyGeneralLanguage v-if="!signedIn && !route.params.projectId && !route.params.erdUuid" class="nc-lang-btn" /> <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 router = useRouter()
const route = router.currentRoute const route = router.currentRoute
const { isLeftSidebarOpen, leftSidebarWidthPercent } = storeToRefs(useSidebarStore()) const {
isLeftSidebarOpen,
leftSidebarWidthPercent,
leftSideBarSize: sideBarSize,
leftSidebarState: sidebarState,
} = storeToRefs(useSidebarStore())
const wrapperRef = ref<HTMLDivElement>() const wrapperRef = ref<HTMLDivElement>()
const sideBarSize = ref({
old: 17.5, const contentSize = computed(() => 100 - sideBarSize.value.current)
current: 17.5, const animationDuration = 250
})
const contentSize = ref({
old: 82.5,
current: 82.5,
})
const isSidebarShort = ref(false)
const animationDuration = 300
const viewportWidth = ref(window.innerWidth) 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 sidebarWidth = computed(() => (sideBarSize.value.old * viewportWidth.value) / 100)
const currentSidebarSize = computed({ const currentSidebarSize = computed({
@ -31,70 +27,46 @@ const currentSidebarSize = computed({
}, },
}) })
watch( watch(currentSidebarSize, () => {
currentSidebarSize, leftSidebarWidthPercent.value = currentSidebarSize.value
() => { })
leftSidebarWidthPercent.value = currentSidebarSize.value
},
{
immediate: true,
},
)
const isSidebarHidden = ref(false)
watch(isLeftSidebarOpen, () => { watch(isLeftSidebarOpen, () => {
sideBarSize.value.current = sideBarSize.value.old sideBarSize.value.current = sideBarSize.value.old
if (isLeftSidebarOpen.value) { if (isLeftSidebarOpen.value) {
contentSize.value.current = contentSize.value.old setTimeout(() => (sidebarState.value = 'openStart'), 0)
setTimeout(() => {
isSidebarShort.value = true
isSidebarHidden.value = false
}, 0)
setTimeout(() => { setTimeout(() => (sidebarState.value = 'openEnd'), animationDuration)
isSidebarShort.value = false
}, animationDuration / 2)
} else { } else {
sideBarSize.value.old = sideBarSize.value.current sideBarSize.value.old = sideBarSize.value.current
contentSize.value.current = contentSize.value.old sidebarState.value = 'hiddenStart'
contentSize.value.current = 100
isSidebarShort.value = true
isAnimationEndAfterSidebarHide.value = false
setTimeout(() => { setTimeout(() => {
isSidebarHidden.value = true
sideBarSize.value.current = 0 sideBarSize.value.current = 0
isAnimationEndAfterSidebarHide.value = true
}, animationDuration * 1.75) sidebarState.value = 'hiddenEnd'
}, animationDuration)
} }
}) })
function handleMouseMove(e: MouseEvent) { function handleMouseMove(e: MouseEvent) {
if (!wrapperRef.value) return if (!wrapperRef.value) return
if (isLeftSidebarOpen.value && !isSidebarHidden.value && !isMouseOverShowSidebarZone.value) return if (sidebarState.value === 'openEnd') return
if (isLeftSidebarOpen.value) {
isSidebarHidden.value = false
isMouseOverShowSidebarZone.value = false
return
}
if (e.clientX < 4) { if (e.clientX < 4 && ['hiddenEnd', 'peekCloseEnd'].includes(sidebarState.value)) {
isSidebarHidden.value = false sidebarState.value = 'peekOpenStart'
isMouseOverShowSidebarZone.value = true
} else if (e.clientX > sidebarWidth.value + 10 && !isSidebarHidden.value) { setTimeout(() => {
isSidebarHidden.value = true sidebarState.value = 'peekOpenEnd'
isMouseOverShowSidebarZone.value = false }, animationDuration)
isAnimationEndAfterSidebarHide.value = false } else if (e.clientX > sidebarWidth.value + 10 && sidebarState.value === 'peekOpenEnd') {
sidebarState.value = 'peekCloseOpen'
setTimeout(() => { setTimeout(() => {
isAnimationEndAfterSidebarHide.value = true sidebarState.value = 'peekCloseEnd'
}, animationDuration * 1.75) }, animationDuration)
} }
} }
@ -113,19 +85,6 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize) window.removeEventListener('resize', onWindowResize)
}) })
watch(
() => !isLeftSidebarOpen.value && isSidebarShort.value,
(value) => {
if (value) {
setTimeout(() => {
isStartHideSidebarAnimation.value = true
}, animationDuration / 2)
} else {
isStartHideSidebarAnimation.value = false
}
},
)
watch(route, () => { watch(route, () => {
if (route.value.name === 'index-index') { if (route.value.name === 'index-index') {
isLeftSidebarOpen.value = true isLeftSidebarOpen.value = true
@ -140,37 +99,32 @@ export default {
</script> </script>
<template> <template>
<NuxtLayout> <NuxtLayout class="h-screen">
<slot v-if="!route.meta.hasSidebar" name="content" /> <slot v-if="!route.meta.hasSidebar" name="content" />
<Splitpanes <Splitpanes
v-else v-else
style="height: 100vh" class="nc-sidebar-content-resizable-wrapper w-full h-full"
class="nc-sidebar-content-resizable-wrapper w-full"
:class="{ :class="{
'sidebar-short': isSidebarShort, 'hide-resize-bar': !isLeftSidebarOpen || sidebarState === 'openStart',
}" }"
@resize="currentSidebarSize = $event[0].size" @resize="currentSidebarSize = $event[0].size"
> >
<Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-sidebar-splitpane relative !overflow-visible"> <Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-sidebar-splitpane relative !overflow-visible">
<div <div
ref="wrapperRef" 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="{ :class="{
'open': isLeftSidebarOpen, 'minimized-height': !isLeftSidebarOpen,
'close': !isLeftSidebarOpen, 'hide-sidebar': ['hiddenStart', 'hiddenEnd', 'peekCloseEnd'].includes(sidebarState),
'absolute': isMouseOverShowSidebarZone,
'sidebar-short': isSidebarShort,
'hide-sidebar': isStartHideSidebarAnimation && !isMouseOverShowSidebarZone,
}" }"
:style="{ :style="{
width: isAnimationEndAfterSidebarHide && isSidebarHidden ? '0px' : `${sidebarWidth}px`, width: sidebarState === 'hiddenEnd' ? '0px' : `${sidebarWidth}px`,
overflow: isMouseOverShowSidebarZone ? 'visible' : undefined,
}" }"
> >
<slot name="sidebar" /> <slot name="sidebar" />
</div> </div>
</Pane> </Pane>
<Pane :size="contentSize.current"> <Pane :size="contentSize">
<slot name="content" /> <slot name="content" />
</Pane> </Pane>
</Splitpanes> </Splitpanes>
@ -178,20 +132,34 @@ export default {
</template> </template>
<style lang="scss"> <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 > { .nc-sidebar-content-resizable-wrapper > {
.splitpanes__splitter { .splitpanes__splitter {
width: 0 !important; @apply !w-0 relative overflow-visible;
position: relative;
overflow: visible;
} }
.splitpanes__splitter:before { .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: ''; content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
z-index: 40;
} }
.splitpanes__splitter:hover:before { .splitpanes__splitter:hover:before {
@ -209,57 +177,25 @@ export default {
.splitpanes--dragging .splitpanes__splitter { .splitpanes--dragging .splitpanes__splitter {
@apply w-1 mr-0; @apply w-1 mr-0;
} }
}
.sidebar-short > .splitpanes__splitter { .nc-sidebar-content-resizable-wrapper.hide-resize-bar > {
display: none !important; .splitpanes__splitter {
cursor: default !important;
opacity: 0 !important;
background-color: transparent !important; background-color: transparent !important;
} }
} }
.splitpanes--dragging { .splitpanes__pane {
cursor: col-resize; transition: width 0.15s ease-in-out !important;
}
.nc-sidebar-wrapper {
@apply flex flex-col h-full justify-center !min-w-32;
} }
.nc-sidebar-wrapper.close { .splitpanes--dragging {
> * { cursor: col-resize;
height: 80vh;
}
}
.nc-sidebar-wrapper.sidebar-short {
> * {
@apply z-10;
height: 80vh !important;
padding-bottom: 0.35rem;
}
}
.nc-sidebar-wrapper.open { > .splitpanes__pane {
height: 100vh; transition: none !important;
> * {
height: 100vh;
} }
} }
.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> </style>

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

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

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

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

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

@ -76,40 +76,12 @@ export default {
</div> </div>
</div> </div>
<a-tooltip placement="bottom"> <div class="flex-1" />
<template #title> Switch language</template>
<LazyGeneralLanguage class="nc-lang-btn" />
</a-tooltip>
</a-layout-header> </a-layout-header>
<div class="w-full overflow-scroll" style="height: calc(100vh)"> <div class="w-full overflow-hidden" style="height: calc(100vh)">
<slot /> <slot />
</div> </div>
</a-layout> </a-layout>
</a-layout> </a-layout>
</template> </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 firstname: string | null
lastname: string | null lastname: string | null
roles: Roles | string roles: Roles | string
project_roles: Roles | string
workspace_roles: Roles | string
invite_token?: string invite_token?: string
project_id?: string project_id?: string
} }
@ -138,7 +140,7 @@ type NcProject = ProjectType & {
interface UndoRedoAction { interface UndoRedoAction {
undo: { fn: Function; args: any[] } undo: { fn: Function; args: any[] }
redo: { fn: Function; args: any[] } redo: { fn: Function; args: any[] }
scope?: { key: string; param: string }[] scope?: { key: string; param: string | string[] }[]
} }
interface ImportWorkerPayload { interface ImportWorkerPayload {
@ -182,6 +184,8 @@ interface Users {
type ViewPageType = 'view' | 'webhook' | 'api' | 'field' | 'relation' type ViewPageType = 'view' | 'webhook' | 'api' | 'field' | 'relation'
type NcButtonSize = 'xxsmall' | 'xsmall' | 'small' | 'medium'
export { export {
User, User,
ProjectMetaInfo, ProjectMetaInfo,
@ -207,4 +211,5 @@ export {
AllRoles, AllRoles,
Users, Users,
ViewPageType, 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 user is still not signed in, redirect to signin page */
if (!state.signedIn.value) return navigateTo('/signin') if (!state.signedIn.value) return navigateTo('/signin')
} else if (to.meta.requiresAuth === false && state.signedIn.value) { } 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) * 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 * redirect to home page

18
packages/nc-gui/package-lock.json generated

@ -54,7 +54,7 @@
"vue-barcode-reader": "^1.0.3", "vue-barcode-reader": "^1.0.3",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
"vue-dompurify-html": "^3.0.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-i18n": "^9.2.2",
"vue-qrcode-reader": "3.1.0-vue3-compatibility.2", "vue-qrcode-reader": "3.1.0-vue3-compatibility.2",
"vue3-calendar-heatmap": "^2.0.0", "vue3-calendar-heatmap": "^2.0.0",
@ -82,7 +82,7 @@
"@iconify-json/mdi": "^1.1.25", "@iconify-json/mdi": "^1.1.25",
"@iconify-json/mi": "^1.1.2", "@iconify-json/mi": "^1.1.2",
"@iconify-json/ph": "^1.1.5", "@iconify-json/ph": "^1.1.5",
"@iconify-json/ri": "^1.1.3", "@iconify-json/ri": "^1.1.12",
"@iconify-json/simple-icons": "^1.1.29", "@iconify-json/simple-icons": "^1.1.29",
"@iconify-json/system-uicons": "^1.1.4", "@iconify-json/system-uicons": "^1.1.4",
"@iconify-json/tabler": "^1.1.59", "@iconify-json/tabler": "^1.1.59",
@ -130,7 +130,7 @@
} }
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.111.0", "version": "0.111.2",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -1584,9 +1584,9 @@
} }
}, },
"node_modules/@iconify-json/ri": { "node_modules/@iconify-json/ri": {
"version": "1.1.10", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.1.10.tgz", "resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.1.12.tgz",
"integrity": "sha512-fsJuXLfpdxb9pfn8LFcfM6LS82j358HaI29Y1qcH1UDnt/V439rTivV6v/YSLkd4z/dZIbh5iOR8+Cg4qtu5Rw==", "integrity": "sha512-s5VsyMWYuUuTB5bATJRoDQKGqo6W0dsxvOBrJI/P2H9QI7IAaAWl8zHxseSjoUGG6AwZwWwDMW9YPULPt+vA6w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@iconify/types": "*" "@iconify/types": "*"
@ -18354,9 +18354,9 @@
} }
}, },
"@iconify-json/ri": { "@iconify-json/ri": {
"version": "1.1.10", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.1.10.tgz", "resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.1.12.tgz",
"integrity": "sha512-fsJuXLfpdxb9pfn8LFcfM6LS82j358HaI29Y1qcH1UDnt/V439rTivV6v/YSLkd4z/dZIbh5iOR8+Cg4qtu5Rw==", "integrity": "sha512-s5VsyMWYuUuTB5bATJRoDQKGqo6W0dsxvOBrJI/P2H9QI7IAaAWl8zHxseSjoUGG6AwZwWwDMW9YPULPt+vA6w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@iconify/types": "*" "@iconify/types": "*"

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

Loading…
Cancel
Save